diff options
Diffstat (limited to 'xbmc/network/httprequesthandler')
27 files changed, 2604 insertions, 0 deletions
diff --git a/xbmc/network/httprequesthandler/CMakeLists.txt b/xbmc/network/httprequesthandler/CMakeLists.txt new file mode 100644 index 0000000..ea514c5 --- /dev/null +++ b/xbmc/network/httprequesthandler/CMakeLists.txt @@ -0,0 +1,30 @@ +if(MICROHTTPD_FOUND) + set(SOURCES HTTPFileHandler.cpp + HTTPImageHandler.cpp + HTTPImageTransformationHandler.cpp + HTTPJsonRpcHandler.cpp + HTTPRequestHandlerUtils.cpp + HTTPVfsHandler.cpp + HTTPWebinterfaceAddonsHandler.cpp + HTTPWebinterfaceHandler.cpp + IHTTPRequestHandler.cpp) + + if(PYTHON_FOUND) + list(APPEND SOURCES HTTPPythonHandler.cpp) + endif() + + set(HEADERS HTTPFileHandler.h + HTTPImageHandler.h + HTTPImageTransformationHandler.h + HTTPJsonRpcHandler.h + HTTPRequestHandlerUtils.h + HTTPVfsHandler.h + HTTPWebinterfaceAddonsHandler.h + HTTPWebinterfaceHandler.h + IHTTPRequestHandler.h) + if(PYTHON_FOUND) + list(APPEND HEADERS HTTPPythonHandler.h) + endif() + + core_add_library(network_httprequesthandlers) +endif() diff --git a/xbmc/network/httprequesthandler/HTTPFileHandler.cpp b/xbmc/network/httprequesthandler/HTTPFileHandler.cpp new file mode 100644 index 0000000..466ab9f --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPFileHandler.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPFileHandler.h" + +#include "filesystem/File.h" +#include "utils/Mime.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +CHTTPFileHandler::CHTTPFileHandler() + : IHTTPRequestHandler(), + m_url(), + m_lastModified() +{ } + +CHTTPFileHandler::CHTTPFileHandler(const HTTPRequest &request) + : IHTTPRequestHandler(request), + m_url(), + m_lastModified() +{ } + +MHD_RESULT CHTTPFileHandler::HandleRequest() +{ + return !m_url.empty() ? MHD_YES : MHD_NO; +} + +bool CHTTPFileHandler::GetLastModifiedDate(CDateTime &lastModified) const +{ + if (!m_lastModified.IsValid()) + return false; + + lastModified = m_lastModified; + return true; +} + +void CHTTPFileHandler::SetFile(const std::string& file, int responseStatus) +{ + m_url = file; + m_response.status = responseStatus; + if (m_url.empty()) + return; + + // translate the response status into the response type + if (m_response.status == MHD_HTTP_OK) + m_response.type = HTTPFileDownload; + else if (m_response.status == MHD_HTTP_FOUND) + m_response.type = HTTPRedirect; + else + m_response.type = HTTPError; + + // try to determine some additional information if the file can be downloaded + if (m_response.type == HTTPFileDownload) + { + // determine the content type + std::string ext = URIUtils::GetExtension(m_url); + StringUtils::ToLower(ext); + m_response.contentType = CMime::GetMimeType(ext); + + // determine the last modified date + XFILE::CFile fileObj; + if (!fileObj.Open(m_url, XFILE::READ_NO_CACHE)) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + } + else + { + struct __stat64 statBuffer; + if (fileObj.Stat(&statBuffer) == 0) + SetLastModifiedDate(&statBuffer); + } + } + + // disable ranges and caching if the file can't be downloaded + if (m_response.type != HTTPFileDownload) + { + m_canHandleRanges = false; + m_canBeCached = false; + } + + // disable caching if the last modified date couldn't be read + if (!m_lastModified.IsValid()) + m_canBeCached = false; +} + +void CHTTPFileHandler::SetLastModifiedDate(const struct __stat64 *statBuffer) +{ + struct tm *time; +#ifdef HAVE_LOCALTIME_R + struct tm result = {}; + time = localtime_r((const time_t*)&statBuffer->st_mtime, &result); +#else + time = localtime((time_t *)&statBuffer->st_mtime); +#endif + if (time != NULL) + m_lastModified = *time; +} diff --git a/xbmc/network/httprequesthandler/HTTPFileHandler.h b/xbmc/network/httprequesthandler/HTTPFileHandler.h new file mode 100644 index 0000000..1562977 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPFileHandler.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +#include <string> + +class CHTTPFileHandler : public IHTTPRequestHandler +{ +public: + ~CHTTPFileHandler() override = default; + + MHD_RESULT HandleRequest() override; + + bool CanHandleRanges() const override { return m_canHandleRanges; } + bool CanBeCached() const override { return m_canBeCached; } + bool GetLastModifiedDate(CDateTime &lastModified) const override; + + std::string GetRedirectUrl() const override { return m_url; } + std::string GetResponseFile() const override { return m_url; } + +protected: + CHTTPFileHandler(); + explicit CHTTPFileHandler(const HTTPRequest &request); + + void SetFile(const std::string& file, int responseStatus); + + void SetCanHandleRanges(bool canHandleRanges) { m_canHandleRanges = canHandleRanges; } + void SetCanBeCached(bool canBeCached) { m_canBeCached = canBeCached; } + void SetLastModifiedDate(const struct __stat64 *buffer); + +private: + std::string m_url; + + bool m_canHandleRanges = true; + bool m_canBeCached = true; + + CDateTime m_lastModified; + +}; diff --git a/xbmc/network/httprequesthandler/HTTPImageHandler.cpp b/xbmc/network/httprequesthandler/HTTPImageHandler.cpp new file mode 100644 index 0000000..6cde13b --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPImageHandler.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPImageHandler.h" + +#include "URL.h" +#include "filesystem/ImageFile.h" +#include "network/WebServer.h" +#include "utils/FileUtils.h" + + +CHTTPImageHandler::CHTTPImageHandler(const HTTPRequest &request) + : CHTTPFileHandler(request) +{ + std::string file; + int responseStatus = MHD_HTTP_BAD_REQUEST; + + // resolve the URL into a file path and a HTTP response status + if (m_request.pathUrl.size() > 7) + { + file = m_request.pathUrl.substr(7); + + XFILE::CImageFile imageFile; + const CURL pathToUrl(file); + if (imageFile.Exists(pathToUrl) && CFileUtils::CheckFileAccessAllowed(file)) + { + responseStatus = MHD_HTTP_OK; + struct __stat64 statBuffer; + if (imageFile.Stat(pathToUrl, &statBuffer) == 0) + { + SetLastModifiedDate(&statBuffer); + SetCanBeCached(true); + } + } + else + responseStatus = MHD_HTTP_NOT_FOUND; + } + + // set the file and the HTTP response status + SetFile(file, responseStatus); +} + +bool CHTTPImageHandler::CanHandleRequest(const HTTPRequest &request) const +{ + return request.pathUrl.find("/image/") == 0; +} diff --git a/xbmc/network/httprequesthandler/HTTPImageHandler.h b/xbmc/network/httprequesthandler/HTTPImageHandler.h new file mode 100644 index 0000000..97df097 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPImageHandler.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "network/httprequesthandler/HTTPFileHandler.h" + +#include <string> + +class CHTTPImageHandler : public CHTTPFileHandler +{ +public: + CHTTPImageHandler() = default; + ~CHTTPImageHandler() override = default; + + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPImageHandler(request); } + bool CanHandleRequest(const HTTPRequest &request) const override; + + int GetPriority() const override { return 5; } + int GetMaximumAgeForCaching() const override { return 60 * 60 * 24 * 7; } + +protected: + explicit CHTTPImageHandler(const HTTPRequest &request); +}; diff --git a/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp new file mode 100644 index 0000000..3d2dccb --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPImageTransformationHandler.h" + +#include "TextureCacheJob.h" +#include "URL.h" +#include "filesystem/ImageFile.h" +#include "network/WebServer.h" +#include "network/httprequesthandler/HTTPRequestHandlerUtils.h" +#include "utils/Mime.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <map> + +#define TRANSFORMATION_OPTION_WIDTH "width" +#define TRANSFORMATION_OPTION_HEIGHT "height" +#define TRANSFORMATION_OPTION_SCALING_ALGORITHM "scaling_algorithm" + +static const std::string ImageBasePath = "/image/"; + +CHTTPImageTransformationHandler::CHTTPImageTransformationHandler() + : m_url(), + m_lastModified(), + m_buffer(NULL), + m_responseData() +{ } + +CHTTPImageTransformationHandler::CHTTPImageTransformationHandler(const HTTPRequest &request) + : IHTTPRequestHandler(request), + m_url(), + m_lastModified(), + m_buffer(NULL), + m_responseData() +{ + m_url = m_request.pathUrl.substr(ImageBasePath.size()); + if (m_url.empty()) + { + m_response.status = MHD_HTTP_BAD_REQUEST; + m_response.type = HTTPError; + return; + } + + XFILE::CImageFile imageFile; + const CURL pathToUrl(m_url); + if (!imageFile.Exists(pathToUrl)) + { + m_response.status = MHD_HTTP_NOT_FOUND; + m_response.type = HTTPError; + return; + } + + m_response.type = HTTPMemoryDownloadNoFreeCopy; + m_response.status = MHD_HTTP_OK; + + // determine the content type + std::string ext = URIUtils::GetExtension(pathToUrl.GetHostName()); + StringUtils::ToLower(ext); + m_response.contentType = CMime::GetMimeType(ext); + + //! @todo determine the maximum age + + // determine the last modified date + struct __stat64 statBuffer; + if (imageFile.Stat(pathToUrl, &statBuffer) != 0) + return; + + struct tm *time; +#ifdef HAVE_LOCALTIME_R + struct tm result = {}; + time = localtime_r((time_t*)&statBuffer.st_mtime, &result); +#else + time = localtime((time_t *)&statBuffer.st_mtime); +#endif + if (time == NULL) + return; + + m_lastModified = *time; +} + +CHTTPImageTransformationHandler::~CHTTPImageTransformationHandler() +{ + m_responseData.clear(); + delete m_buffer; + m_buffer = NULL; +} + +bool CHTTPImageTransformationHandler::CanHandleRequest(const HTTPRequest &request) const +{ + if ((request.method != GET && request.method != HEAD) || + request.pathUrl.find(ImageBasePath) != 0 || request.pathUrl.size() <= ImageBasePath.size()) + return false; + + // get the transformation options + std::map<std::string, std::string> options; + HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_GET_ARGUMENT_KIND, options); + + return (options.find(TRANSFORMATION_OPTION_WIDTH) != options.end() || + options.find(TRANSFORMATION_OPTION_HEIGHT) != options.end()); +} + +MHD_RESULT CHTTPImageTransformationHandler::HandleRequest() +{ + if (m_response.type == HTTPError) + return MHD_YES; + + // get the transformation options + std::map<std::string, std::string> options; + HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, options); + + std::vector<std::string> urlOptions; + std::map<std::string, std::string>::const_iterator option = options.find(TRANSFORMATION_OPTION_WIDTH); + if (option != options.end()) + urlOptions.push_back(TRANSFORMATION_OPTION_WIDTH "=" + option->second); + + option = options.find(TRANSFORMATION_OPTION_HEIGHT); + if (option != options.end()) + urlOptions.push_back(TRANSFORMATION_OPTION_HEIGHT "=" + option->second); + + option = options.find(TRANSFORMATION_OPTION_SCALING_ALGORITHM); + if (option != options.end()) + urlOptions.push_back(TRANSFORMATION_OPTION_SCALING_ALGORITHM "=" + option->second); + + std::string imagePath = m_url; + if (!urlOptions.empty()) + { + imagePath += "?"; + imagePath += StringUtils::Join(urlOptions, "&"); + } + + // resize the image into the local buffer + size_t bufferSize; + if (!CTextureCacheJob::ResizeTexture(imagePath, m_buffer, bufferSize)) + { + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + m_response.type = HTTPError; + + return MHD_YES; + } + + // store the size of the image + m_response.totalLength = bufferSize; + + // nothing else to do if the request is not ranged + if (!GetRequestedRanges(m_response.totalLength)) + { + m_responseData.push_back(CHttpResponseRange(m_buffer, 0, m_response.totalLength - 1)); + return MHD_YES; + } + + for (HttpRanges::const_iterator range = m_request.ranges.Begin(); range != m_request.ranges.End(); ++range) + m_responseData.push_back(CHttpResponseRange(m_buffer + range->GetFirstPosition(), range->GetFirstPosition(), range->GetLastPosition())); + + return MHD_YES; +} + +bool CHTTPImageTransformationHandler::GetLastModifiedDate(CDateTime &lastModified) const +{ + if (!m_lastModified.IsValid()) + return false; + + lastModified = m_lastModified; + return true; +} diff --git a/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h new file mode 100644 index 0000000..6d8732d --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +#include <stdint.h> +#include <string> + +class CHTTPImageTransformationHandler : public IHTTPRequestHandler +{ +public: + CHTTPImageTransformationHandler(); + ~CHTTPImageTransformationHandler() override; + + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPImageTransformationHandler(request); } + bool CanHandleRequest(const HTTPRequest &request)const override; + + MHD_RESULT HandleRequest() override; + + bool CanHandleRanges() const override { return true; } + bool CanBeCached() const override { return true; } + bool GetLastModifiedDate(CDateTime &lastModified) const override; + + HttpResponseRanges GetResponseData() const override { return m_responseData; } + + // priority must be higher than the one of CHTTPImageHandler + int GetPriority() const override { return 6; } + +protected: + explicit CHTTPImageTransformationHandler(const HTTPRequest &request); + +private: + std::string m_url; + CDateTime m_lastModified; + + uint8_t* m_buffer; + HttpResponseRanges m_responseData; +}; diff --git a/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp new file mode 100644 index 0000000..9cfdbc5 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPJsonRpcHandler.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "interfaces/json-rpc/JSONRPC.h" +#include "interfaces/json-rpc/JSONServiceDescription.h" +#include "network/httprequesthandler/HTTPRequestHandlerUtils.h" +#include "utils/FileUtils.h" +#include "utils/JSONVariantWriter.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#define MAX_HTTP_POST_SIZE 65536 + +bool CHTTPJsonRpcHandler::CanHandleRequest(const HTTPRequest &request) const +{ + return (request.pathUrl.compare("/jsonrpc") == 0); +} + +MHD_RESULT CHTTPJsonRpcHandler::HandleRequest() +{ + CHTTPClient client(m_request.method); + bool isRequest = false; + std::string jsonpCallback; + + // get all query arguments + std::map<std::string, std::string> arguments; + HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, arguments); + + if (m_request.method == POST) + { + std::string contentType = HTTPRequestHandlerUtils::GetRequestHeaderValue(m_request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE); + // If the content-type of the m_request was specified, it must be application/json-rpc, application/json, or application/jsonrequest + // http://www.jsonrpc.org/historical/json-rpc-over-http.html + if (!contentType.empty() && contentType.compare("application/json-rpc") != 0 && + contentType.compare("application/json") != 0 && contentType.compare("application/jsonrequest") != 0) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_UNSUPPORTED_MEDIA_TYPE; + return MHD_YES; + } + + isRequest = true; + } + else if (m_request.method == GET || m_request.method == HEAD) + { + std::map<std::string, std::string>::const_iterator argument = arguments.find("request"); + if (argument != arguments.end() && !argument->second.empty()) + { + m_requestData = argument->second; + isRequest = true; + } + } + + std::map<std::string, std::string>::const_iterator argument = arguments.find("jsonp"); + if (argument != arguments.end() && !argument->second.empty()) + jsonpCallback = argument->second; + else + { + argument = arguments.find("callback"); + if (argument != arguments.end() && !argument->second.empty()) + jsonpCallback = argument->second; + } + + if (isRequest) + { + m_responseData = JSONRPC::CJSONRPC::MethodCall(m_requestData, &m_transportLayer, &client); + + if (!jsonpCallback.empty()) + m_responseData = jsonpCallback + "(" + m_responseData + ");"; + } + else if (jsonpCallback.empty()) + { + // get the whole output of JSONRPC.Introspect + CVariant result; + JSONRPC::CJSONServiceDescription::Print(result, &m_transportLayer, &client); + if (!CJSONVariantWriter::Write(result, m_responseData, false)) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + + return MHD_YES; + } + } + else + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_BAD_REQUEST; + + return MHD_YES; + } + + m_requestData.clear(); + + m_responseRange.SetData(m_responseData.c_str(), m_responseData.size()); + + m_response.type = HTTPMemoryDownloadNoFreeCopy; + m_response.status = MHD_HTTP_OK; + m_response.contentType = "application/json"; + m_response.totalLength = m_responseData.size(); + + return MHD_YES; +} + +HttpResponseRanges CHTTPJsonRpcHandler::GetResponseData() const +{ + HttpResponseRanges ranges; + ranges.push_back(m_responseRange); + + return ranges; +} + +bool CHTTPJsonRpcHandler::appendPostData(const char *data, size_t size) +{ + if (m_requestData.size() + size > MAX_HTTP_POST_SIZE) + { + CServiceBroker::GetLogging() + .GetLogger("CHTTPJsonRpcHandler") + ->error("Stopped uploading POST data since it exceeded size limitations ({})", + MAX_HTTP_POST_SIZE); + return false; + } + + m_requestData.append(data, size); + + return true; +} + +bool CHTTPJsonRpcHandler::CHTTPTransportLayer::PrepareDownload(const char *path, CVariant &details, std::string &protocol) +{ + if (!CFileUtils::Exists(path)) + return false; + + protocol = "http"; + std::string url; + std::string strPath = path; + if (StringUtils::StartsWith(strPath, "image://") || + (StringUtils::StartsWith(strPath, "special://") && StringUtils::EndsWith(strPath, ".tbn"))) + url = "image/"; + else + url = "vfs/"; + url += CURL::Encode(strPath); + details["path"] = url; + + return true; +} + +bool CHTTPJsonRpcHandler::CHTTPTransportLayer::Download(const char *path, CVariant &result) +{ + return false; +} + +int CHTTPJsonRpcHandler::CHTTPTransportLayer::GetCapabilities() +{ + return JSONRPC::Response | JSONRPC::FileDownloadRedirect; +} + +CHTTPJsonRpcHandler::CHTTPClient::CHTTPClient(HTTPMethod method) + : m_permissionFlags(JSONRPC::ReadData) +{ + // with a HTTP POST request everything is allowed + if (method == POST) + m_permissionFlags = JSONRPC::OPERATION_PERMISSION_ALL; +} + +int CHTTPJsonRpcHandler::CHTTPClient::GetAnnouncementFlags() +{ + // Does not support broadcast + return 0; +} + +bool CHTTPJsonRpcHandler::CHTTPClient::SetAnnouncementFlags(int flags) +{ + return false; +} diff --git a/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h new file mode 100644 index 0000000..88d4496 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/json-rpc/IClient.h" +#include "interfaces/json-rpc/ITransportLayer.h" +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +#include <string> + +class CHTTPJsonRpcHandler : public IHTTPRequestHandler +{ +public: + CHTTPJsonRpcHandler() = default; + ~CHTTPJsonRpcHandler() override = default; + + // implementations of IHTTPRequestHandler + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPJsonRpcHandler(request); } + bool CanHandleRequest(const HTTPRequest &request) const override; + + MHD_RESULT HandleRequest() override; + + HttpResponseRanges GetResponseData() const override; + + int GetPriority() const override { return 5; } + +protected: + explicit CHTTPJsonRpcHandler(const HTTPRequest &request) + : IHTTPRequestHandler(request) + { } + + bool appendPostData(const char *data, size_t size) override; + +private: + std::string m_requestData; + std::string m_responseData; + CHttpResponseRange m_responseRange; + + class CHTTPTransportLayer : public JSONRPC::ITransportLayer + { + public: + CHTTPTransportLayer() = default; + ~CHTTPTransportLayer() override = default; + + // implementations of JSONRPC::ITransportLayer + bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) override; + bool Download(const char *path, CVariant &result) override; + int GetCapabilities() override; + }; + CHTTPTransportLayer m_transportLayer; + + class CHTTPClient : public JSONRPC::IClient + { + public: + explicit CHTTPClient(HTTPMethod method); + ~CHTTPClient() override = default; + + int GetPermissionFlags() override { return m_permissionFlags; } + int GetAnnouncementFlags() override; + bool SetAnnouncementFlags(int flags) override; + + private: + int m_permissionFlags; + }; +}; diff --git a/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp b/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp new file mode 100644 index 0000000..8633744 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPPythonHandler.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "addons/Webinterface.h" +#include "addons/addoninfo/AddonType.h" +#include "filesystem/File.h" +#include "interfaces/generic/ScriptInvocationManager.h" +#include "interfaces/python/XBPython.h" +#include "network/WebServer.h" +#include "network/httprequesthandler/HTTPRequestHandlerUtils.h" +#include "network/httprequesthandler/HTTPWebinterfaceHandler.h" +#include "network/httprequesthandler/python/HTTPPythonInvoker.h" +#include "network/httprequesthandler/python/HTTPPythonWsgiInvoker.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#define MAX_STRING_POST_SIZE 20000 + +CHTTPPythonHandler::CHTTPPythonHandler() + : IHTTPRequestHandler(), + m_scriptPath(), + m_addon(), + m_lastModified(), + m_requestData(), + m_responseData(), + m_responseRanges(), + m_redirectUrl() +{ } + +CHTTPPythonHandler::CHTTPPythonHandler(const HTTPRequest &request) + : IHTTPRequestHandler(request), + m_scriptPath(), + m_addon(), + m_lastModified(), + m_requestData(), + m_responseData(), + m_responseRanges(), + m_redirectUrl() +{ + m_response.type = HTTPMemoryDownloadNoFreeCopy; + + // get the real path of the script and check if it actually exists + m_response.status = CHTTPWebinterfaceHandler::ResolveUrl(m_request.pathUrl, m_scriptPath, m_addon); + // only allow requests to a non-static webinterface addon + if (m_addon == NULL || m_addon->Type() != ADDON::AddonType::WEB_INTERFACE || + std::dynamic_pointer_cast<ADDON::CWebinterface>(m_addon)->GetType() == + ADDON::WebinterfaceTypeStatic) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + + return; + } + + std::shared_ptr<ADDON::CWebinterface> webinterface = std::dynamic_pointer_cast<ADDON::CWebinterface>(m_addon); + + // forward every request to the default entry point + m_scriptPath = webinterface->LibPath(); + + // we need to map any requests to a specific WSGI webinterface to the root path + std::string baseLocation = webinterface->GetBaseLocation(); + if (!URIUtils::PathHasParent(m_request.pathUrl, baseLocation)) + { + m_response.type = HTTPRedirect; + m_response.status = MHD_HTTP_MOVED_PERMANENTLY; + m_redirectUrl = baseLocation + m_request.pathUrl; + } + + // no need to try to read the last modified date from a non-existing file + if (m_response.status != MHD_HTTP_OK) + return; + + // determine the last modified date + const CURL pathToUrl(m_scriptPath); + struct __stat64 statBuffer; + if (XFILE::CFile::Stat(pathToUrl, &statBuffer) != 0) + return; + + struct tm* time; +#ifdef HAVE_LOCALTIME_R + struct tm result = {}; + time = localtime_r((time_t*)&statBuffer.st_mtime, &result); +#else + time = localtime((time_t *)&statBuffer.st_mtime); +#endif + if (time == NULL) + return; + + m_lastModified = *time; +} + +bool CHTTPPythonHandler::CanHandleRequest(const HTTPRequest &request) const +{ + ADDON::AddonPtr addon; + std::string path; + // try to resolve the addon as any python script must be part of a webinterface + if (!CHTTPWebinterfaceHandler::ResolveAddon(request.pathUrl, addon, path) || addon == NULL || + addon->Type() != ADDON::AddonType::WEB_INTERFACE) + return false; + + // static webinterfaces aren't allowed to run python scripts + ADDON::CWebinterface* webinterface = static_cast<ADDON::CWebinterface*>(addon.get()); + if (webinterface->GetType() != ADDON::WebinterfaceTypeWsgi) + return false; + + return true; +} + +MHD_RESULT CHTTPPythonHandler::HandleRequest() +{ + if (m_response.type == HTTPError || m_response.type == HTTPRedirect) + return MHD_YES; + + std::vector<std::string> args; + args.push_back(m_scriptPath); + + try + { + HTTPPythonRequest* pythonRequest = new HTTPPythonRequest(); + pythonRequest->connection = m_request.connection; + pythonRequest->file = URIUtils::GetFileName(m_request.pathUrl); + HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, pythonRequest->getValues); + HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_HEADER_KIND, pythonRequest->headerValues); + pythonRequest->method = m_request.method; + pythonRequest->postValues = m_postFields; + pythonRequest->requestContent = m_requestData; + pythonRequest->responseType = HTTPNone; + pythonRequest->responseLength = 0; + pythonRequest->responseStatus = MHD_HTTP_OK; + pythonRequest->url = m_request.pathUrlFull; + pythonRequest->path = m_request.pathUrl; + pythonRequest->version = m_request.version; + pythonRequest->requestTime = CDateTime::GetCurrentDateTime(); + pythonRequest->lastModifiedTime = m_lastModified; + + std::string hostname; + uint16_t port; + if (GetHostnameAndPort(hostname, port)) + { + pythonRequest->hostname = hostname; + pythonRequest->port = port; + } + + CHTTPPythonInvoker* pythonInvoker = + new CHTTPPythonWsgiInvoker(&CServiceBroker::GetXBPython(), pythonRequest); + LanguageInvokerPtr languageInvokerPtr(pythonInvoker); + int result = CScriptInvocationManager::GetInstance().ExecuteSync(m_scriptPath, languageInvokerPtr, m_addon, args, 30000, false); + + // check if the script couldn't be started + if (result < 0) + { + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + m_response.type = HTTPError; + + return MHD_YES; + } + // check if the script exited with an error + if (result > 0) + { + // check if the script didn't finish in time + if (result == ETIMEDOUT) + m_response.status = MHD_HTTP_REQUEST_TIMEOUT; + else + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + m_response.type = HTTPError; + + return MHD_YES; + } + + HTTPPythonRequest* pythonFinalizedRequest = pythonInvoker->GetRequest(); + if (pythonFinalizedRequest == NULL) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + return MHD_YES; + } + + m_response.type = pythonFinalizedRequest->responseType; + m_response.status = pythonFinalizedRequest->responseStatus; + if (m_response.status < MHD_HTTP_BAD_REQUEST) + { + if (m_response.type == HTTPNone) + m_response.type = HTTPMemoryDownloadNoFreeCopy; + m_response.headers = pythonFinalizedRequest->responseHeaders; + + if (pythonFinalizedRequest->lastModifiedTime.IsValid()) + m_lastModified = pythonFinalizedRequest->lastModifiedTime; + } + else + { + if (m_response.type == HTTPNone) + m_response.type = HTTPError; + m_response.headers = pythonFinalizedRequest->responseHeadersError; + } + + m_responseData = pythonFinalizedRequest->responseData; + if (pythonFinalizedRequest->responseLength > 0 && pythonFinalizedRequest->responseLength <= m_responseData.size()) + m_response.totalLength = pythonFinalizedRequest->responseLength; + else + m_response.totalLength = m_responseData.size(); + + CHttpResponseRange responseRange(m_responseData.c_str(), m_responseData.size()); + m_responseRanges.push_back(responseRange); + + if (!pythonFinalizedRequest->responseContentType.empty()) + m_response.contentType = pythonFinalizedRequest->responseContentType; + } + catch (...) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + } + + return MHD_YES; +} + +bool CHTTPPythonHandler::GetLastModifiedDate(CDateTime &lastModified) const +{ + if (!m_lastModified.IsValid()) + return false; + + lastModified = m_lastModified; + return true; +} + +bool CHTTPPythonHandler::appendPostData(const char *data, size_t size) +{ + if (m_requestData.size() + size > MAX_STRING_POST_SIZE) + { + CServiceBroker::GetLogging() + .GetLogger("CHTTPPythonHandler") + ->error("Stopped uploading post since it exceeded size limitations ({})", + MAX_STRING_POST_SIZE); + return false; + } + + m_requestData.append(data, size); + + return true; +} diff --git a/xbmc/network/httprequesthandler/HTTPPythonHandler.h b/xbmc/network/httprequesthandler/HTTPPythonHandler.h new file mode 100644 index 0000000..166430e --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPPythonHandler.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" +#include "addons/IAddon.h" +#include "addons/Webinterface.h" +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +class CHTTPPythonHandler : public IHTTPRequestHandler +{ +public: + CHTTPPythonHandler(); + ~CHTTPPythonHandler() override = default; + + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPPythonHandler(request); } + bool CanHandleRequest(const HTTPRequest &request) const override; + bool CanHandleRanges() const override { return false; } + bool CanBeCached() const override { return false; } + bool GetLastModifiedDate(CDateTime &lastModified) const override; + + MHD_RESULT HandleRequest() override; + + HttpResponseRanges GetResponseData() const override { return m_responseRanges; } + + std::string GetRedirectUrl() const override { return m_redirectUrl; } + + int GetPriority() const override { return 3; } + +protected: + explicit CHTTPPythonHandler(const HTTPRequest &request); + + bool appendPostData(const char *data, size_t size) override; + +private: + std::string m_scriptPath; + ADDON::AddonPtr m_addon; + CDateTime m_lastModified; + + std::string m_requestData; + std::string m_responseData; + HttpResponseRanges m_responseRanges; + + std::string m_redirectUrl; +}; diff --git a/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp new file mode 100644 index 0000000..240449a --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPRequestHandlerUtils.h" + +#include "utils/StringUtils.h" + +#include <map> + +std::string HTTPRequestHandlerUtils::GetRequestHeaderValue(struct MHD_Connection *connection, enum MHD_ValueKind kind, const std::string &key) +{ + if (connection == nullptr) + return ""; + + const char* value = MHD_lookup_connection_value(connection, kind, key.c_str()); + if (value == nullptr) + return ""; + + if (StringUtils::EqualsNoCase(key, MHD_HTTP_HEADER_CONTENT_TYPE)) + { + // Work around a bug in firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=416178) + // by cutting of anything that follows a ";" in a "Content-Type" header field + std::string strValue(value); + size_t pos = strValue.find(';'); + if (pos != std::string::npos) + strValue = strValue.substr(0, pos); + + return strValue; + } + + return value; +} + +int HTTPRequestHandlerUtils::GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::map<std::string, std::string> &headerValues) +{ + if (connection == nullptr) + return -1; + + return MHD_get_connection_values(connection, kind, FillArgumentMap, &headerValues); +} + +int HTTPRequestHandlerUtils::GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::multimap<std::string, std::string> &headerValues) +{ + if (connection == nullptr) + return -1; + + return MHD_get_connection_values(connection, kind, FillArgumentMultiMap, &headerValues); +} + +bool HTTPRequestHandlerUtils::GetRequestedRanges(struct MHD_Connection *connection, uint64_t totalLength, CHttpRanges &ranges) +{ + ranges.Clear(); + + if (connection == nullptr) + return false; + + return ranges.Parse(GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE), totalLength); +} + +MHD_RESULT HTTPRequestHandlerUtils::FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value) +{ + if (cls == nullptr || key == nullptr) + return MHD_NO; + + std::map<std::string, std::string> *arguments = reinterpret_cast<std::map<std::string, std::string>*>(cls); + arguments->insert(std::make_pair(key, value != nullptr ? value : "")); + + return MHD_YES; +} + +MHD_RESULT HTTPRequestHandlerUtils::FillArgumentMultiMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value) +{ + if (cls == nullptr || key == nullptr) + return MHD_NO; + + std::multimap<std::string, std::string> *arguments = reinterpret_cast<std::multimap<std::string, std::string>*>(cls); + arguments->insert(std::make_pair(key, value != nullptr ? value : "")); + + return MHD_YES; +} diff --git a/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h new file mode 100644 index 0000000..d02b5c1 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +#include <stdint.h> +#include <string> + +class HTTPRequestHandlerUtils +{ +public: + static std::string GetRequestHeaderValue(struct MHD_Connection *connection, enum MHD_ValueKind kind, const std::string &key); + static int GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::map<std::string, std::string> &headerValues); + static int GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::multimap<std::string, std::string> &headerValues); + + static bool GetRequestedRanges(struct MHD_Connection *connection, uint64_t totalLength, CHttpRanges &ranges); + +private: + HTTPRequestHandlerUtils() = delete; + + static MHD_RESULT FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value); + static MHD_RESULT FillArgumentMultiMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value); +}; diff --git a/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp b/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp new file mode 100644 index 0000000..ef2f7a6 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPVfsHandler.h" + +#include "MediaSource.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "media/MediaLockState.h" +#include "settings/MediaSourceSettings.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" + +CHTTPVfsHandler::CHTTPVfsHandler(const HTTPRequest &request) + : CHTTPFileHandler(request) +{ + std::string file; + int responseStatus = MHD_HTTP_BAD_REQUEST; + + if (m_request.pathUrl.size() > 5) + { + file = m_request.pathUrl.substr(5); + + if (CFileUtils::Exists(file)) + { + bool accessible = false; + if (file.substr(0, 8) == "image://") + accessible = true; + else + { + std::string sourceTypes[] = { "video", "music", "pictures" }; + unsigned int size = sizeof(sourceTypes) / sizeof(std::string); + + std::string realPath = URIUtils::GetRealPath(file); + // for rar:// and zip:// paths we need to extract the path to the archive instead of using the VFS path + while (URIUtils::IsInArchive(realPath)) + realPath = CURL(realPath).GetHostName(); + + // Check manually configured sources + VECSOURCES *sources = NULL; + for (unsigned int index = 0; index < size && !accessible; index++) + { + sources = CMediaSourceSettings::GetInstance().GetSources(sourceTypes[index]); + if (sources == NULL) + continue; + + for (const auto& source : *sources) + { + if (accessible) + break; + + // don't allow access to locked / disabled sharing sources + if (source.m_iHasLock == LOCK_STATE_LOCKED || !source.m_allowSharing) + continue; + + for (const auto& path : source.vecPaths) + { + std::string realSourcePath = URIUtils::GetRealPath(path); + if (URIUtils::PathHasParent(realPath, realSourcePath, true)) + { + accessible = true; + break; + } + } + } + } + + // Check auto-mounted sources + if (!accessible) + { + bool isSource; + VECSOURCES removableSources; + CServiceBroker::GetMediaManager().GetRemovableDrives(removableSources); + int sourceIndex = CUtil::GetMatchingSource(realPath, removableSources, isSource); + if (sourceIndex >= 0 && sourceIndex < static_cast<int>(removableSources.size()) && + removableSources.at(sourceIndex).m_iHasLock != LOCK_STATE_LOCKED && + removableSources.at(sourceIndex).m_allowSharing) + accessible = true; + } + } + + if (accessible) + responseStatus = MHD_HTTP_OK; + // the file exists but not in one of the defined sources so we deny access to it + else + responseStatus = MHD_HTTP_UNAUTHORIZED; + } + else + responseStatus = MHD_HTTP_NOT_FOUND; + } + + // set the file and the HTTP response status + SetFile(file, responseStatus); +} + +bool CHTTPVfsHandler::CanHandleRequest(const HTTPRequest &request) const +{ + return request.pathUrl.find("/vfs") == 0; +} diff --git a/xbmc/network/httprequesthandler/HTTPVfsHandler.h b/xbmc/network/httprequesthandler/HTTPVfsHandler.h new file mode 100644 index 0000000..af66bad --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPVfsHandler.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "network/httprequesthandler/HTTPFileHandler.h" + +#include <string> + +class CHTTPVfsHandler : public CHTTPFileHandler +{ +public: + CHTTPVfsHandler() = default; + ~CHTTPVfsHandler() override = default; + + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPVfsHandler(request); } + bool CanHandleRequest(const HTTPRequest &request) const override; + + int GetPriority() const override { return 5; } + +protected: + explicit CHTTPVfsHandler(const HTTPRequest &request); +}; diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp new file mode 100644 index 0000000..fa47144 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPWebinterfaceAddonsHandler.h" + +#include "ServiceBroker.h" +#include "addons/Addon.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonType.h" +#include "network/WebServer.h" + +#define ADDON_HEADER "<html><head><title>Add-on List</title></head><body>\n<h1>Available web interfaces:</h1>\n<ul>\n" + +bool CHTTPWebinterfaceAddonsHandler::CanHandleRequest(const HTTPRequest &request) const +{ + return (request.pathUrl.compare("/addons") == 0 || request.pathUrl.compare("/addons/") == 0); +} + +MHD_RESULT CHTTPWebinterfaceAddonsHandler::HandleRequest() +{ + m_responseData = ADDON_HEADER; + ADDON::VECADDONS addons; + if (!CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::WEB_INTERFACE) || + addons.empty()) + { + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + + return MHD_YES; + } + + for (const auto& addon : addons) + m_responseData += "<li><a href=/addons/" + addon->ID() + "/>" + addon->Name() + "</a></li>\n"; + + m_responseData += "</ul>\n</body></html>"; + + m_responseRange.SetData(m_responseData.c_str(), m_responseData.size()); + + m_response.type = HTTPMemoryDownloadNoFreeCopy; + m_response.status = MHD_HTTP_OK; + m_response.contentType = "text/html"; + m_response.totalLength = m_responseData.size(); + + return MHD_YES; +} + +HttpResponseRanges CHTTPWebinterfaceAddonsHandler::GetResponseData() const +{ + HttpResponseRanges ranges; + ranges.push_back(m_responseRange); + + return ranges; +} + + diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h new file mode 100644 index 0000000..20a44ec --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +#include <string> + +class CHTTPWebinterfaceAddonsHandler : public IHTTPRequestHandler +{ +public: + CHTTPWebinterfaceAddonsHandler() = default; + ~CHTTPWebinterfaceAddonsHandler() override = default; + + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPWebinterfaceAddonsHandler(request); } + bool CanHandleRequest(const HTTPRequest &request) const override; + + MHD_RESULT HandleRequest() override; + + HttpResponseRanges GetResponseData() const override; + + int GetPriority() const override { return 4; } + +protected: + explicit CHTTPWebinterfaceAddonsHandler(const HTTPRequest &request) + : IHTTPRequestHandler(request) + { } + +private: + std::string m_responseData; + CHttpResponseRange m_responseRange; +}; diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp new file mode 100644 index 0000000..fe6e760 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPWebinterfaceHandler.h" + +#include "ServiceBroker.h" +#include "addons/AddonManager.h" +#include "addons/AddonSystemSettings.h" +#include "addons/Webinterface.h" +#include "addons/addoninfo/AddonType.h" +#include "filesystem/Directory.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#define WEBSERVER_DIRECTORY_SEPARATOR "/" + +CHTTPWebinterfaceHandler::CHTTPWebinterfaceHandler(const HTTPRequest &request) + : CHTTPFileHandler(request) +{ + // resolve the URL into a file path and a HTTP response status + std::string file; + int responseStatus = ResolveUrl(request.pathUrl, file); + + // set the file and the HTTP response status + SetFile(file, responseStatus); +} + +bool CHTTPWebinterfaceHandler::CanHandleRequest(const HTTPRequest &request) const +{ + return true; +} + +int CHTTPWebinterfaceHandler::ResolveUrl(const std::string &url, std::string &path) +{ + ADDON::AddonPtr dummyAddon; + return ResolveUrl(url, path, dummyAddon); +} + +int CHTTPWebinterfaceHandler::ResolveUrl(const std::string &url, std::string &path, ADDON::AddonPtr &addon) +{ + // determine the addon and addon's path + if (!ResolveAddon(url, addon, path)) + return MHD_HTTP_NOT_FOUND; + + if (XFILE::CDirectory::Exists(path)) + { + if (URIUtils::GetFileName(path).empty()) + { + // determine the actual file path using the default entry point + if (addon != NULL && addon->Type() == ADDON::AddonType::WEB_INTERFACE) + path = std::dynamic_pointer_cast<ADDON::CWebinterface>(addon)->GetEntryPoint(path); + } + else + { + URIUtils::AddSlashAtEnd(path); + return MHD_HTTP_FOUND; + } + } + + if (!CFileUtils::CheckFileAccessAllowed(path)) + return MHD_HTTP_NOT_FOUND; + + if (!CFileUtils::Exists(path)) + return MHD_HTTP_NOT_FOUND; + + return MHD_HTTP_OK; +} + +bool CHTTPWebinterfaceHandler::ResolveAddon(const std::string &url, ADDON::AddonPtr &addon) +{ + std::string addonPath; + return ResolveAddon(url, addon, addonPath); +} + +bool CHTTPWebinterfaceHandler::ResolveAddon(const std::string &url, ADDON::AddonPtr &addon, std::string &addonPath) +{ + std::string path = url; + + // check if the URL references a specific addon + if (url.find("/addons/") == 0 && url.size() > 8) + { + std::vector<std::string> components; + StringUtils::Tokenize(path, components, WEBSERVER_DIRECTORY_SEPARATOR); + if (components.size() <= 1) + return false; + + if (!CServiceBroker::GetAddonMgr().GetAddon(components.at(1), addon, + ADDON::OnlyEnabled::CHOICE_YES) || + addon == NULL) + return false; + + addonPath = addon->Path(); + if (addon->Type() != + ADDON::AddonType::WEB_INTERFACE) // No need to append /htdocs for web interfaces + addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/"); + + // remove /addons/<addon-id> from the path + components.erase(components.begin(), components.begin() + 2); + + // determine the path within the addon + path = StringUtils::Join(components, WEBSERVER_DIRECTORY_SEPARATOR); + } + else if (!ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::WEB_INTERFACE, + addon) || + addon == NULL) + return false; + + // get the path of the addon + addonPath = addon->Path(); + + // add /htdocs/ to the addon's path if it's not a webinterface + if (addon->Type() != ADDON::AddonType::WEB_INTERFACE) + addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/"); + + // append the path within the addon to the path of the addon + addonPath = URIUtils::AddFileToFolder(addonPath, path); + + // ensure that we don't have a directory traversal hack here + // by checking if the resolved absolute path is inside the addon path + std::string realPath = URIUtils::GetRealPath(addonPath); + std::string realAddonPath = URIUtils::GetRealPath(addon->Path()); + if (!URIUtils::PathHasParent(realPath, realAddonPath, true)) + return false; + + return true; +} diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h new file mode 100644 index 0000000..5618c75 --- /dev/null +++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" +#include "network/httprequesthandler/HTTPFileHandler.h" + +#include <string> + +class CHTTPWebinterfaceHandler : public CHTTPFileHandler +{ +public: + CHTTPWebinterfaceHandler() = default; + ~CHTTPWebinterfaceHandler() override = default; + + IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPWebinterfaceHandler(request); } + bool CanHandleRequest(const HTTPRequest &request) const override; + + static int ResolveUrl(const std::string &url, std::string &path); + static int ResolveUrl(const std::string &url, std::string &path, ADDON::AddonPtr &addon); + static bool ResolveAddon(const std::string &url, ADDON::AddonPtr &addon); + static bool ResolveAddon(const std::string &url, ADDON::AddonPtr &addon, std::string &addonPath); + +protected: + explicit CHTTPWebinterfaceHandler(const HTTPRequest &request); +}; diff --git a/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp b/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp new file mode 100644 index 0000000..3a06604 --- /dev/null +++ b/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "IHTTPRequestHandler.h" + +#include "network/WebServer.h" +#include "network/httprequesthandler/HTTPRequestHandlerUtils.h" +#include "utils/StringUtils.h" + +#include <limits> +#include <utility> + +static const std::string HTTPMethodHead = "HEAD"; +static const std::string HTTPMethodGet = "GET"; +static const std::string HTTPMethodPost = "POST"; + +HTTPMethod GetHTTPMethod(const char *method) +{ + if (HTTPMethodGet.compare(method) == 0) + return GET; + if (HTTPMethodPost.compare(method) == 0) + return POST; + if (HTTPMethodHead.compare(method) == 0) + return HEAD; + + return UNKNOWN; +} + +std::string GetHTTPMethod(HTTPMethod method) +{ + switch (method) + { + case HEAD: + return HTTPMethodHead; + + case GET: + return HTTPMethodGet; + + case POST: + return HTTPMethodPost; + + case UNKNOWN: + break; + } + + return ""; +} + +IHTTPRequestHandler::IHTTPRequestHandler() + : m_request(), + m_response(), + m_postFields() +{ } + +IHTTPRequestHandler::IHTTPRequestHandler(const HTTPRequest &request) + : m_request(request), + m_response(), + m_postFields() +{ + m_response.type = HTTPError; + m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR; + m_response.totalLength = 0; +} + +bool IHTTPRequestHandler::HasResponseHeader(const std::string &field) const +{ + if (field.empty()) + return false; + + return m_response.headers.find(field) != m_response.headers.end(); +} + +bool IHTTPRequestHandler::AddResponseHeader(const std::string &field, const std::string &value, bool allowMultiple /* = false */) +{ + if (field.empty() || value.empty()) + return false; + + if (!allowMultiple && HasResponseHeader(field)) + return false; + + m_response.headers.insert(std::make_pair(field, value)); + return true; +} + +void IHTTPRequestHandler::AddPostField(const std::string &key, const std::string &value) +{ + if (key.empty()) + return; + + std::map<std::string, std::string>::iterator field = m_postFields.find(key); + if (field == m_postFields.end()) + m_postFields[key] = value; + else + m_postFields[key].append(value); +} + +bool IHTTPRequestHandler::AddPostData(const char *data, size_t size) +{ + if (size > 0) + return appendPostData(data, size); + + return true; +} + +bool IHTTPRequestHandler::GetRequestedRanges(uint64_t totalLength) +{ + if (!m_ranged || m_request.webserver == NULL || m_request.connection == NULL) + return false; + + m_request.ranges.Clear(); + if (totalLength == 0) + return true; + + return HTTPRequestHandlerUtils::GetRequestedRanges(m_request.connection, totalLength, m_request.ranges); +} + +bool IHTTPRequestHandler::GetHostnameAndPort(std::string& hostname, uint16_t &port) +{ + if (m_request.webserver == NULL || m_request.connection == NULL) + return false; + + std::string hostnameAndPort = HTTPRequestHandlerUtils::GetRequestHeaderValue(m_request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_HOST); + if (hostnameAndPort.empty()) + return false; + + size_t pos = hostnameAndPort.find(':'); + hostname = hostnameAndPort.substr(0, pos); + if (hostname.empty()) + return false; + + if (pos != std::string::npos) + { + std::string strPort = hostnameAndPort.substr(pos + 1); + if (!StringUtils::IsNaturalNumber(strPort)) + return false; + + unsigned long portL = strtoul(strPort.c_str(), NULL, 0); + if (portL > std::numeric_limits<uint16_t>::max()) + return false; + + port = static_cast<uint16_t>(portL); + } + else + port = 80; + + return true; +} diff --git a/xbmc/network/httprequesthandler/IHTTPRequestHandler.h b/xbmc/network/httprequesthandler/IHTTPRequestHandler.h new file mode 100644 index 0000000..13c170f --- /dev/null +++ b/xbmc/network/httprequesthandler/IHTTPRequestHandler.h @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/HttpRangeUtils.h" + +#include <map> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <string> + +#include <microhttpd.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/types.h> + +#if MHD_VERSION >= 0x00097002 +using MHD_RESULT = MHD_Result; +#else +using MHD_RESULT = int; +#endif + +class CDateTime; +class CWebServer; + +enum HTTPMethod +{ + UNKNOWN, + POST, + GET, + HEAD +}; + +HTTPMethod GetHTTPMethod(const char *method); +std::string GetHTTPMethod(HTTPMethod method); + +typedef enum HTTPResponseType +{ + HTTPNone, + // creates and returns a HTTP error + HTTPError, + // creates and returns a HTTP redirect response + HTTPRedirect, + // creates a HTTP response with the content from a file + HTTPFileDownload, + // creates a HTTP response from a buffer without copying or freeing the buffer + HTTPMemoryDownloadNoFreeNoCopy, + // creates a HTTP response from a buffer by copying but not freeing the buffer + HTTPMemoryDownloadNoFreeCopy, + // creates a HTTP response from a buffer without copying followed by freeing the buffer + // the buffer must have been malloc'ed and not new'ed + HTTPMemoryDownloadFreeNoCopy, + // creates a HTTP response from a buffer by copying followed by freeing the buffer + // the buffer must have been malloc'ed and not new'ed + HTTPMemoryDownloadFreeCopy +} HTTPResponseType; + +typedef struct HTTPRequest +{ + CWebServer *webserver; + struct MHD_Connection *connection; + std::string pathUrlFull; + std::string pathUrl; + HTTPMethod method; + std::string version; + CHttpRanges ranges; +} HTTPRequest; + +typedef struct HTTPResponseDetails { + HTTPResponseType type; + int status; + std::multimap<std::string, std::string> headers; + std::string contentType; + uint64_t totalLength; +} HTTPResponseDetails; + +class IHTTPRequestHandler +{ +public: + virtual ~IHTTPRequestHandler() = default; + + /*! + * \brief Creates a new HTTP request handler for the given request. + * + * \details This call is responsible for doing some preparation work like - + * depending on the supported features - determining whether the requested + * entity supports ranges, whether it can be cached and what it's last + * modified date is. + * + * \param request HTTP request to be handled + */ + virtual IHTTPRequestHandler* Create(const HTTPRequest &request) const = 0; + + /*! + * \brief Returns the priority of the HTTP request handler. + * + * \details The higher the priority the more important is the HTTP request + * handler. + */ + virtual int GetPriority() const { return 0; } + + /*! + * \brief Checks if the HTTP request handler can handle the given request. + * + * \param request HTTP request to be handled + * \return True if the given HTTP request can be handled otherwise false. + */ + virtual bool CanHandleRequest(const HTTPRequest &request) const = 0; + + /*! + * \brief Handles the HTTP request. + * + * \return MHD_NO if a severe error has occurred otherwise MHD_YES. + */ + virtual MHD_RESULT HandleRequest() = 0; + + /*! + * \brief Whether the HTTP response could also be provided in ranges. + */ + virtual bool CanHandleRanges() const { return false; } + + /*! + * \brief Whether the HTTP response can be cached. + */ + virtual bool CanBeCached() const { return false; } + + /*! + * \brief Returns the maximum age (in seconds) for which the response can be cached. + * + * \details This is only used if the response can be cached. + */ + virtual int GetMaximumAgeForCaching() const { return 0; } + + /*! + * \brief Returns the last modification date of the response data. + * + * \details This is only used if the response can be cached. + */ + virtual bool GetLastModifiedDate(CDateTime &lastModified) const { return false; } + + /*! + * \brief Returns the ranges with raw data belonging to the response. + * + * \details This is only used if the response type is one of the HTTPMemoryDownload types. + */ + virtual HttpResponseRanges GetResponseData() const { return HttpResponseRanges(); } + + /*! + * \brief Returns the URL to which the request should be redirected. + * + * \details This is only used if the response type is HTTPRedirect. + */ + virtual std::string GetRedirectUrl() const { return ""; } + + /*! + * \brief Returns the path to the file that should be returned as the response. + * + * \details This is only used if the response type is HTTPFileDownload. + */ + virtual std::string GetResponseFile() const { return ""; } + + /*! + * \brief Returns the HTTP request handled by the HTTP request handler. + */ + const HTTPRequest& GetRequest() const { return m_request; } + + /*! + * \brief Returns true if the HTTP request is ranged, otherwise false. + */ + bool IsRequestRanged() const { return m_ranged; } + + /*! + * \brief Sets whether the HTTP request contains ranges or not + */ + void SetRequestRanged(bool ranged) { m_ranged = ranged; } + + /*! + * \brief Sets the response status of the HTTP response. + * + * \param status HTTP status of the response + */ + void SetResponseStatus(int status) { m_response.status = status; } + + /*! + * \brief Checks if the given HTTP header field is part of the response details. + * + * \param field HTTP header field name + * \return True if the header field is set, otherwise false. + */ + bool HasResponseHeader(const std::string &field) const; + + /*! + * \brief Adds the given HTTP header field and value to the response details. + * + * \param field HTTP header field name + * \param value HTTP header field value + * \param allowMultiple Whether the same header is allowed multiple times + * \return True if the header field was added, otherwise false. + */ + bool AddResponseHeader(const std::string &field, const std::string &value, bool allowMultiple = false); + + /*! + * \brief Returns the HTTP response header details. + */ + const HTTPResponseDetails& GetResponseDetails() const { return m_response; } + + /*! + * \brief Adds the given key-value pair extracted from the HTTP POST data. + * + * \param key Key of the HTTP POST field + * \param value Value of the HTTP POST field + */ + void AddPostField(const std::string &key, const std::string &value); + /*! + * \brief Adds the given raw HTTP POST data. + * + * \param data Raw HTTP POST data + * \param size Size of the raw HTTP POST data + */ + bool AddPostData(const char *data, size_t size); + +protected: + IHTTPRequestHandler(); + explicit IHTTPRequestHandler(const HTTPRequest &request); + + virtual bool appendPostData(const char *data, size_t size) + { return true; } + + bool GetRequestedRanges(uint64_t totalLength); + bool GetHostnameAndPort(std::string& hostname, uint16_t &port); + + HTTPRequest m_request; + HTTPResponseDetails m_response; + + std::map<std::string, std::string> m_postFields; + +private: + bool m_ranged = false; +}; diff --git a/xbmc/network/httprequesthandler/python/CMakeLists.txt b/xbmc/network/httprequesthandler/python/CMakeLists.txt new file mode 100644 index 0000000..7bbbad9 --- /dev/null +++ b/xbmc/network/httprequesthandler/python/CMakeLists.txt @@ -0,0 +1,10 @@ +if(MICROHTTPD_FOUND AND PYTHON_FOUND) + set(SOURCES HTTPPythonInvoker.cpp + HTTPPythonWsgiInvoker.cpp) + + set(HEADERS HTTPPythonInvoker.h + HTTPPythonRequest.h + HTTPPythonWsgiInvoker.h) + + core_add_library(network_httprequesthandlers_python) +endif() diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp new file mode 100644 index 0000000..696c039 --- /dev/null +++ b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPPythonInvoker.h" + +#include "CompileInfo.h" +#include "utils/StringUtils.h" + +CHTTPPythonInvoker::CHTTPPythonInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request) + : CPythonInvoker(invocationHandler), + m_request(request), + m_internalError(false) +{ } + +CHTTPPythonInvoker::~CHTTPPythonInvoker() +{ + delete m_request; + m_request = NULL; +} + +void CHTTPPythonInvoker::onAbort() +{ + if (m_request == NULL) + return; + + m_internalError = true; + m_request->responseType = HTTPError; + m_request->responseStatus = MHD_HTTP_INTERNAL_SERVER_ERROR; +} + +void CHTTPPythonInvoker::onError(const std::string& exceptionType /* = "" */, const std::string& exceptionValue /* = "" */, const std::string& exceptionTraceback /* = "" */) +{ + if (m_request == NULL) + return; + + m_internalError = true; + m_request->responseType = HTTPMemoryDownloadNoFreeCopy; + m_request->responseStatus = MHD_HTTP_INTERNAL_SERVER_ERROR; + + std::string output; + if (!exceptionType.empty()) + { + output += exceptionType; + + if (!exceptionValue.empty()) + output += ": " + exceptionValue; + output += "\n"; + } + + if (!exceptionTraceback .empty()) + output += exceptionTraceback; + + // replace all special characters + + StringUtils::Replace(output, "<", "<"); + StringUtils::Replace(output, ">", ">"); + StringUtils::Replace(output, " ", " "); + StringUtils::Replace(output, "\n", "\n<br />"); + + if (!exceptionType.empty()) + { + // now make the type and value bold (needs to be done here because otherwise the < and > would have been replaced + output = "<b>" + output; + output.insert(output.find('\n'), "</b>"); + } + + m_request->responseData = "<html><head><title>" + std::string(CCompileInfo::GetAppName()) + ": python error</title></head><body>" + output + "</body></html>"; +} diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h new file mode 100644 index 0000000..83f1f34 --- /dev/null +++ b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/python/PythonInvoker.h" +#include "network/httprequesthandler/python/HTTPPythonRequest.h" + +#include <string> + +class CHTTPPythonInvoker : public CPythonInvoker +{ +public: + ~CHTTPPythonInvoker() override; + + virtual HTTPPythonRequest* GetRequest() = 0; + +protected: + CHTTPPythonInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request); + + // overrides of CPythonInvoker + void onAbort() override; + void onError(const std::string& exceptionType = "", const std::string& exceptionValue = "", const std::string& exceptionTraceback = "") override; + + HTTPPythonRequest* m_request; + bool m_internalError; +}; diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h b/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h new file mode 100644 index 0000000..7874203 --- /dev/null +++ b/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" +#include "network/httprequesthandler/IHTTPRequestHandler.h" + +#include <map> +#include <stdint.h> +#include <string> + +typedef struct HTTPPythonRequest +{ + struct MHD_Connection *connection; + std::string hostname; + uint16_t port; + std::string url; + std::string path; + std::string file; + HTTPMethod method; + std::string version; + std::multimap<std::string, std::string> headerValues; + std::map<std::string, std::string> getValues; + std::map<std::string, std::string> postValues; + std::string requestContent; + CDateTime requestTime; + CDateTime lastModifiedTime; + + HTTPResponseType responseType; + int responseStatus; + std::string responseContentType; + std::string responseData; + size_t responseLength; + std::multimap<std::string, std::string> responseHeaders; + std::multimap<std::string, std::string> responseHeadersError; +} HTTPPythonRequest; diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp new file mode 100644 index 0000000..ac047a7 --- /dev/null +++ b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPPythonWsgiInvoker.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "addons/Webinterface.h" +#include "addons/addoninfo/AddonType.h" +#include "interfaces/legacy/wsgi/WsgiErrorStream.h" +#include "interfaces/legacy/wsgi/WsgiInputStream.h" +#include "interfaces/legacy/wsgi/WsgiResponse.h" +#include "interfaces/python/swig.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <utility> + +#include <Python.h> + +#define MODULE "xbmc" + +#define RUNSCRIPT_PREAMBLE \ + "" \ + "import " MODULE "\n" \ + "class xbmcout:\n" \ + " def __init__(self, loglevel=" MODULE ".LOGINFO):\n" \ + " self.ll=loglevel\n" \ + " def write(self, data):\n" \ + " " MODULE ".log(data,self.ll)\n" \ + " def close(self):\n" \ + " " MODULE ".log('.')\n" \ + " def flush(self):\n" \ + " " MODULE ".log('.')\n" \ + "import sys\n" \ + "sys.stdout = xbmcout()\n" \ + "sys.stderr = xbmcout(" MODULE ".LOGERROR)\n" \ + "" + +#define RUNSCRIPT_SETUPTOOLS_HACK \ + "" \ + "import types,sys\n" \ + "pkg_resources_code = \\\n" \ + "\"\"\"\n" \ + "def resource_filename(__name__,__path__):\n" \ + " return __path__\n" \ + "\"\"\"\n" \ + "pkg_resources = types.ModuleType('pkg_resources')\n" \ + "exec(pkg_resources_code, pkg_resources.__dict__)\n" \ + "sys.modules['pkg_resources'] = pkg_resources\n" \ + "" + +#define RUNSCRIPT_POSTSCRIPT \ + MODULE ".log('-->HTTP Python WSGI Interpreter Initialized<--', " MODULE ".LOGINFO)\n" \ + "" + +#if defined(TARGET_ANDROID) +#define RUNSCRIPT \ + RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUPTOOLS_HACK RUNSCRIPT_POSTSCRIPT +#else +#define RUNSCRIPT \ + RUNSCRIPT_PREAMBLE RUNSCRIPT_POSTSCRIPT +#endif + +namespace PythonBindings { +PyObject* PyInit_Module_xbmc(void); +PyObject* PyInit_Module_xbmcaddon(void); +PyObject* PyInit_Module_xbmcwsgi(void); +} + +using namespace PythonBindings; + +typedef struct +{ + const char *name; + CPythonInvoker::PythonModuleInitialization initialization; +} PythonModule; + +static PythonModule PythonModules[] = +{ + { "xbmc", PyInit_Module_xbmc }, + { "xbmcaddon", PyInit_Module_xbmcaddon }, + { "xbmcwsgi", PyInit_Module_xbmcwsgi } +}; + +CHTTPPythonWsgiInvoker::CHTTPPythonWsgiInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request) + : CHTTPPythonInvoker(invocationHandler, request), + m_wsgiResponse(NULL) +{ + PyImport_AppendInittab("xbmc", PyInit_Module_xbmc); + PyImport_AppendInittab("xbmcaddon", PyInit_Module_xbmcaddon); + PyImport_AppendInittab("xbmcwsgi", PyInit_Module_xbmcwsgi); +} + +CHTTPPythonWsgiInvoker::~CHTTPPythonWsgiInvoker() +{ + delete m_wsgiResponse; + m_wsgiResponse = NULL; +} + +HTTPPythonRequest* CHTTPPythonWsgiInvoker::GetRequest() +{ + if (m_request == NULL || m_wsgiResponse == NULL) + return NULL; + + if (m_internalError) + return m_request; + + m_wsgiResponse->Finalize(m_request); + return m_request; +} + +void CHTTPPythonWsgiInvoker::executeScript(FILE* fp, const std::string& script, PyObject* moduleDict) +{ + if (m_request == NULL || m_addon == NULL || m_addon->Type() != ADDON::AddonType::WEB_INTERFACE || + fp == NULL || script.empty() || moduleDict == NULL) + return; + + auto logger = CServiceBroker::GetLogging().GetLogger( + StringUtils::Format("CHTTPPythonWsgiInvoker[{}]", script)); + + ADDON::CWebinterface* webinterface = static_cast<ADDON::CWebinterface*>(m_addon.get()); + if (webinterface->GetType() != ADDON::WebinterfaceTypeWsgi) + { + logger->error("trying to execute a non-WSGI script"); + return; + } + + PyObject* pyScript = NULL; + PyObject* pyModule = NULL; + PyObject* pyEntryPoint = NULL; + std::map<std::string, std::string> cgiEnvironment; + PyObject* pyEnviron = NULL; + PyObject* pyStart_response = NULL; + PyObject* pyArgs = NULL; + PyObject* pyResult = NULL; + PyObject* pyResultIterator = NULL; + PyObject* pyIterResult = NULL; + + // get the script + std::string scriptName = URIUtils::GetFileName(script); + URIUtils::RemoveExtension(scriptName); + pyScript = PyUnicode_FromStringAndSize(scriptName.c_str(), scriptName.size()); + if (pyScript == NULL) + { + logger->error("failed to convert script to python string"); + return; + } + + // load the script + logger->debug("loading script"); + pyModule = PyImport_Import(pyScript); + Py_DECREF(pyScript); + if (pyModule == NULL) + { + logger->error("failed to load WSGI script"); + return; + } + + // get the entry point + const std::string& entryPoint = webinterface->EntryPoint(); + logger->debug(R"(loading entry point "{}")", entryPoint); + pyEntryPoint = PyObject_GetAttrString(pyModule, entryPoint.c_str()); + if (pyEntryPoint == NULL) + { + logger->error(R"(failed to load entry point "{}")", entryPoint); + goto cleanup; + } + + // check if the loaded entry point is a callable function + if (!PyCallable_Check(pyEntryPoint)) + { + logger->error(R"(defined entry point "{}" is not callable)", entryPoint); + goto cleanup; + } + + // prepare the WsgiResponse object + m_wsgiResponse = new XBMCAddon::xbmcwsgi::WsgiResponse(); + if (m_wsgiResponse == NULL) + { + logger->error("failed to create WsgiResponse object"); + goto cleanup; + } + + try + { + // prepare the start_response callable + pyStart_response = PythonBindings::makePythonInstance(m_wsgiResponse, true); + + // create the (CGI) environment dictionary + cgiEnvironment = createCgiEnvironment(m_request, m_addon); + // and turn it into a python dictionary + pyEnviron = PyDict_New(); + for (const auto& cgiEnv : cgiEnvironment) + { + PyObject* pyEnvEntry = PyUnicode_FromStringAndSize(cgiEnv.second.c_str(), cgiEnv.second.size()); + PyDict_SetItemString(pyEnviron, cgiEnv.first.c_str(), pyEnvEntry); + Py_DECREF(pyEnvEntry); + } + + // add the WSGI-specific environment variables + addWsgiEnvironment(m_request, pyEnviron); + } + catch (const XBMCAddon::WrongTypeException& e) + { + logger->error("failed to prepare WsgiResponse object with wrong type exception: {}", + e.GetExMessage()); + goto cleanup; + } + catch (const XbmcCommons::Exception& e) + { + logger->error("failed to prepare WsgiResponse object with exception: {}", e.GetExMessage()); + goto cleanup; + } + catch (...) + { + logger->error("failed to prepare WsgiResponse object with unknown exception"); + goto cleanup; + } + + // put together the arguments + pyArgs = PyTuple_Pack(2, pyEnviron, pyStart_response); + Py_DECREF(pyEnviron); + Py_DECREF(pyStart_response); + + // call the given handler with the prepared arguments + pyResult = PyObject_CallObject(pyEntryPoint, pyArgs); + Py_DECREF(pyArgs); + if (pyResult == NULL) + { + logger->error("no result"); + goto cleanup; + } + + // try to get an iterator from the result object + pyResultIterator = PyObject_GetIter(pyResult); + if (pyResultIterator == NULL || !PyIter_Check(pyResultIterator)) + { + logger->error("result is not iterable"); + goto cleanup; + } + + // go through all the iterables in the result and turn them into strings + while ((pyIterResult = PyIter_Next(pyResultIterator)) != NULL) + { + std::string result; + try + { + PythonBindings::PyXBMCGetUnicodeString(result, pyIterResult, false, "result", "handle_request"); + } + catch (const XBMCAddon::WrongTypeException& e) + { + logger->error("failed to parse result iterable object with wrong type exception: {}", + e.GetExMessage()); + goto cleanup; + } + catch (const XbmcCommons::Exception& e) + { + logger->error("failed to parse result iterable object with exception: {}", e.GetExMessage()); + goto cleanup; + } + catch (...) + { + logger->error("failed to parse result iterable object with unknown exception"); + goto cleanup; + } + + // append the result string to the response + m_wsgiResponse->Append(result); + } + +cleanup: + if (pyIterResult != NULL) + { + Py_DECREF(pyIterResult); + } + if (pyResultIterator != NULL) + { + // Call optional close method on iterator + if (PyObject_HasAttrString(pyResultIterator, "close") == 1) + { + if (PyObject_CallMethod(pyResultIterator, "close", NULL) == NULL) + logger->error("failed to close iterator object"); + } + Py_DECREF(pyResultIterator); + } + if (pyResult != NULL) + { + Py_DECREF(pyResult); + } + if (pyEntryPoint != NULL) + { + Py_DECREF(pyEntryPoint); + } + if (pyModule != NULL) + { + Py_DECREF(pyModule); + } +} + +std::map<std::string, CPythonInvoker::PythonModuleInitialization> CHTTPPythonWsgiInvoker::getModules() const +{ + static std::map<std::string, PythonModuleInitialization> modules; + if (modules.empty()) + { + for (const PythonModule& pythonModule : PythonModules) + modules.insert(std::make_pair(pythonModule.name, pythonModule.initialization)); + } + + return modules; +} + +const char* CHTTPPythonWsgiInvoker::getInitializationScript() const +{ + return RUNSCRIPT; +} + +std::map<std::string, std::string> CHTTPPythonWsgiInvoker::createCgiEnvironment( + const HTTPPythonRequest* httpRequest, const ADDON::AddonPtr& addon) +{ + std::map<std::string, std::string> environment; + + // REQUEST_METHOD + std::string requestMethod; + switch (httpRequest->method) + { + case HEAD: + requestMethod = "HEAD"; + break; + + case POST: + requestMethod = "POST"; + break; + + case GET: + default: + requestMethod = "GET"; + break; + } + environment.insert(std::make_pair("REQUEST_METHOD", requestMethod)); + + // SCRIPT_NAME + std::string scriptName = std::dynamic_pointer_cast<ADDON::CWebinterface>(addon)->GetBaseLocation(); + environment.insert(std::make_pair("SCRIPT_NAME", scriptName)); + + // PATH_INFO + std::string pathInfo = httpRequest->path.substr(scriptName.size()); + environment.insert(std::make_pair("PATH_INFO", pathInfo)); + + // QUERY_STRING + size_t iOptions = httpRequest->url.find_first_of('?'); + if (iOptions != std::string::npos) + environment.insert(std::make_pair("QUERY_STRING", httpRequest->url.substr(iOptions+1))); + else + environment.insert(std::make_pair("QUERY_STRING", "")); + + // CONTENT_TYPE + std::string headerValue; + std::multimap<std::string, std::string>::const_iterator headerIt = httpRequest->headerValues.find(MHD_HTTP_HEADER_CONTENT_TYPE); + if (headerIt != httpRequest->headerValues.end()) + headerValue = headerIt->second; + environment.insert(std::make_pair("CONTENT_TYPE", headerValue)); + + // CONTENT_LENGTH + headerValue.clear(); + headerIt = httpRequest->headerValues.find(MHD_HTTP_HEADER_CONTENT_LENGTH); + if (headerIt != httpRequest->headerValues.end()) + headerValue = headerIt->second; + environment.insert(std::make_pair("CONTENT_LENGTH", headerValue)); + + // SERVER_NAME + environment.insert(std::make_pair("SERVER_NAME", httpRequest->hostname)); + + // SERVER_PORT + environment.insert(std::make_pair("SERVER_PORT", std::to_string(httpRequest->port))); + + // SERVER_PROTOCOL + environment.insert(std::make_pair("SERVER_PROTOCOL", httpRequest->version)); + + // HTTP_<HEADER_NAME> + for (headerIt = httpRequest->headerValues.begin(); headerIt != httpRequest->headerValues.end(); ++headerIt) + { + std::string headerName = headerIt->first; + StringUtils::ToUpper(headerName); + environment.insert(std::make_pair("HTTP_" + headerName, headerIt->second)); + } + + return environment; +} + +void CHTTPPythonWsgiInvoker::addWsgiEnvironment(HTTPPythonRequest* request, void* environment) +{ + if (environment == nullptr) + return; + + PyObject* pyEnviron = reinterpret_cast<PyObject*>(environment); + if (pyEnviron == nullptr) + return; + + // WSGI-defined variables + { + // wsgi.version + PyObject* pyValue = Py_BuildValue("(ii)", 1, 0); + PyDict_SetItemString(pyEnviron, "wsgi.version", pyValue); + Py_DECREF(pyValue); + } + { + // wsgi.url_scheme + PyObject* pyValue = PyUnicode_FromStringAndSize("http", 4); + PyDict_SetItemString(pyEnviron, "wsgi.url_scheme", pyValue); + Py_DECREF(pyValue); + } + { + // wsgi.input + XBMCAddon::xbmcwsgi::WsgiInputStream* wsgiInputStream = new XBMCAddon::xbmcwsgi::WsgiInputStream(); + if (request != NULL) + wsgiInputStream->SetRequest(request); + + PythonBindings::prepareForReturn(wsgiInputStream); + PyObject* pyWsgiInputStream = PythonBindings::makePythonInstance(wsgiInputStream, false); + PyDict_SetItemString(pyEnviron, "wsgi.input", pyWsgiInputStream); + Py_DECREF(pyWsgiInputStream); + } + { + // wsgi.errors + XBMCAddon::xbmcwsgi::WsgiErrorStream* wsgiErrorStream = new XBMCAddon::xbmcwsgi::WsgiErrorStream(); + if (request != NULL) + wsgiErrorStream->SetRequest(request); + + PythonBindings::prepareForReturn(wsgiErrorStream); + PyObject* pyWsgiErrorStream = PythonBindings::makePythonInstance(wsgiErrorStream, false); + PyDict_SetItemString(pyEnviron, "wsgi.errors", pyWsgiErrorStream); + Py_DECREF(pyWsgiErrorStream); + } + { + // wsgi.multithread + PyObject* pyValue = Py_BuildValue("b", false); + PyDict_SetItemString(pyEnviron, "wsgi.multithread", pyValue); + Py_DECREF(pyValue); + } + { + // wsgi.multiprocess + PyObject* pyValue = Py_BuildValue("b", false); + PyDict_SetItemString(pyEnviron, "wsgi.multiprocess", pyValue); + Py_DECREF(pyValue); + } + { + // wsgi.run_once + PyObject* pyValue = Py_BuildValue("b", true); + PyDict_SetItemString(pyEnviron, "wsgi.run_once", pyValue); + Py_DECREF(pyValue); + } +} diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h new file mode 100644 index 0000000..3ec34c0 --- /dev/null +++ b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/python/PythonInvoker.h" +#include "network/httprequesthandler/python/HTTPPythonInvoker.h" +#include "network/httprequesthandler/python/HTTPPythonRequest.h" + +#include <map> +#include <string> + +namespace XBMCAddon +{ + namespace xbmcwsgi + { + class WsgiResponse; + } +} + +class CHTTPPythonWsgiInvoker : public CHTTPPythonInvoker +{ +public: + CHTTPPythonWsgiInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request); + ~CHTTPPythonWsgiInvoker() override; + + // implementations of CHTTPPythonInvoker + HTTPPythonRequest* GetRequest() override; + +protected: + // overrides of CPythonInvoker + void executeScript(FILE* fp, const std::string& script, PyObject* moduleDict) override; + std::map<std::string, PythonModuleInitialization> getModules() const override; + const char* getInitializationScript() const override; + +private: + static std::map<std::string, std::string> createCgiEnvironment( + const HTTPPythonRequest* httpRequest, const ADDON::AddonPtr& addon); + static void addWsgiEnvironment(HTTPPythonRequest* request, void* environment); + + XBMCAddon::xbmcwsgi::WsgiResponse* m_wsgiResponse; +}; |