summaryrefslogtreecommitdiffstats
path: root/xbmc/network/WebServer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/network/WebServer.cpp')
-rw-r--r--xbmc/network/WebServer.cpp1409
1 files changed, 1409 insertions, 0 deletions
diff --git a/xbmc/network/WebServer.cpp b/xbmc/network/WebServer.cpp
new file mode 100644
index 0000000..0ee1696
--- /dev/null
+++ b/xbmc/network/WebServer.cpp
@@ -0,0 +1,1409 @@
+/*
+ * Copyright (C) 2005-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 "WebServer.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "filesystem/File.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <utility>
+
+#if defined(TARGET_POSIX)
+#include <pthread.h>
+#endif
+
+#include <inttypes.h>
+
+#define MAX_POST_BUFFER_SIZE 2048
+
+#define PAGE_FILE_NOT_FOUND \
+ "<html><head><title>File not found</title></head><body>File not found</body></html>"
+#define NOT_SUPPORTED \
+ "<html><head><title>Not Supported</title></head><body>The method you are trying to use is not " \
+ "supported by this server</body></html>"
+
+#define HEADER_VALUE_NO_CACHE "no-cache"
+
+#define HEADER_NEWLINE "\r\n"
+
+typedef struct
+{
+ std::shared_ptr<XFILE::CFile> file;
+ CHttpRanges ranges;
+ size_t rangeCountTotal;
+ std::string boundary;
+ std::string boundaryWithHeader;
+ std::string boundaryEnd;
+ bool boundaryWritten;
+ std::string contentType;
+ uint64_t writePosition;
+} HttpFileDownloadContext;
+
+CWebServer::CWebServer()
+ : m_authenticationUsername("kodi"),
+ m_authenticationPassword(""),
+ m_key(),
+ m_cert(),
+ m_logger(CServiceBroker::GetLogging().GetLogger("CWebServer"))
+{
+#if defined(TARGET_DARWIN)
+ void* stack_addr;
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_getstack(&attr, &stack_addr, &m_thread_stacksize);
+ pthread_attr_destroy(&attr);
+ // double the stack size under darwin, not sure why yet
+ // but it stopped crashing using Kodi iOS remote -> play video.
+ // non-darwin will pass a value of zero which means 'system default'
+ m_thread_stacksize *= 2;
+ m_logger->debug("increasing thread stack to {}", m_thread_stacksize);
+#endif
+}
+
+static MHD_Response* create_response(size_t size, const void* data, int free, int copy)
+{
+ MHD_ResponseMemoryMode mode = MHD_RESPMEM_PERSISTENT;
+ if (copy)
+ mode = MHD_RESPMEM_MUST_COPY;
+ else if (free)
+ mode = MHD_RESPMEM_MUST_FREE;
+ //! @bug libmicrohttpd isn't const correct
+ return MHD_create_response_from_buffer(size, const_cast<void*>(data), mode);
+}
+
+MHD_RESULT CWebServer::AskForAuthentication(const HTTPRequest& request) const
+{
+ struct MHD_Response* response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (!response)
+ {
+ m_logger->error("unable to create HTTP Unauthorized response");
+ return MHD_NO;
+ }
+
+ MHD_RESULT ret = AddHeader(response, MHD_HTTP_HEADER_CONNECTION, "close");
+ if (!ret)
+ {
+ m_logger->error("unable to prepare HTTP Unauthorized response");
+ MHD_destroy_response(response);
+ return MHD_NO;
+ }
+
+ LogResponse(request, MHD_HTTP_UNAUTHORIZED);
+
+ // This MHD_RESULT cast is only necessary for libmicrohttpd 0.9.71
+ // The return type of MHD_queue_basic_auth_fail_response was fixed for future versions
+ // See
+ // https://git.gnunet.org/libmicrohttpd.git/commit/?id=860b42e9180da4dcd7e8690a3fcdb4e37e5772c5
+ ret = static_cast<MHD_RESULT>(
+ MHD_queue_basic_auth_fail_response(request.connection, CCompileInfo::GetAppName(), response));
+ MHD_destroy_response(response);
+
+ return ret;
+}
+
+bool CWebServer::IsAuthenticated(const HTTPRequest& request) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_authenticationRequired)
+ return true;
+
+ // try to retrieve username and password for basic authentication
+ char* password = nullptr;
+ char* username = MHD_basic_auth_get_username_password(request.connection, &password);
+
+ if (username == nullptr || password == nullptr)
+ return false;
+
+ // compare the received username and password
+ bool authenticated = m_authenticationUsername.compare(username) == 0 &&
+ m_authenticationPassword.compare(password) == 0;
+
+ free(username);
+ free(password);
+
+ return authenticated;
+}
+
+MHD_RESULT CWebServer::AnswerToConnection(void* cls,
+ struct MHD_Connection* connection,
+ const char* url,
+ const char* method,
+ const char* version,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls)
+{
+ if (cls == nullptr || con_cls == nullptr || *con_cls == nullptr)
+ {
+ GetLogger()->error("invalid request received");
+ return MHD_NO;
+ }
+
+ CWebServer* webServer = reinterpret_cast<CWebServer*>(cls);
+
+ ConnectionHandler* connectionHandler = reinterpret_cast<ConnectionHandler*>(*con_cls);
+ HTTPMethod methodType = GetHTTPMethod(method);
+ HTTPRequest request = {webServer, connection, connectionHandler->fullUri, url, methodType,
+ version, {}};
+
+ if (connectionHandler->isNew)
+ webServer->LogRequest(request);
+
+ return webServer->HandlePartialRequest(connection, connectionHandler, request, upload_data,
+ upload_data_size, con_cls);
+}
+
+MHD_RESULT CWebServer::HandlePartialRequest(struct MHD_Connection* connection,
+ ConnectionHandler* connectionHandler,
+ const HTTPRequest& request,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls)
+{
+ std::unique_ptr<ConnectionHandler> conHandler(connectionHandler);
+
+ // remember if the request was new
+ bool isNewRequest = conHandler->isNew;
+ // because now it isn't anymore
+ conHandler->isNew = false;
+
+ // reset con_cls and set it if still necessary
+ *con_cls = nullptr;
+
+ if (!IsAuthenticated(request))
+ return AskForAuthentication(request);
+
+ // check if this is the first call to AnswerToConnection for this request
+ if (isNewRequest)
+ {
+ // look for a IHTTPRequestHandler which can take care of the current request
+ auto handler = FindRequestHandler(request);
+ if (handler != nullptr)
+ {
+ // if we got a GET request we need to check if it should be cached
+ if (request.method == GET || request.method == HEAD)
+ {
+ if (handler->CanBeCached())
+ {
+ bool cacheable = IsRequestCacheable(request);
+
+ CDateTime lastModified;
+ if (handler->GetLastModifiedDate(lastModified) && lastModified.IsValid())
+ {
+ // handle If-Modified-Since or If-Unmodified-Since
+ std::string ifModifiedSince = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MODIFIED_SINCE);
+ std::string ifUnmodifiedSince = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE);
+
+ CDateTime ifModifiedSinceDate;
+ CDateTime ifUnmodifiedSinceDate;
+ // handle If-Modified-Since (but only if the response is cacheable)
+ if (cacheable && ifModifiedSinceDate.SetFromRFC1123DateTime(ifModifiedSince) &&
+ lastModified.GetAsUTCDateTime() <= ifModifiedSinceDate)
+ {
+ struct MHD_Response* response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP 304 response");
+ return MHD_NO;
+ }
+
+ return FinalizeRequest(handler, MHD_HTTP_NOT_MODIFIED, response);
+ }
+ // handle If-Unmodified-Since
+ else if (ifUnmodifiedSinceDate.SetFromRFC1123DateTime(ifUnmodifiedSince) &&
+ lastModified.GetAsUTCDateTime() > ifUnmodifiedSinceDate)
+ return SendErrorResponse(request, MHD_HTTP_PRECONDITION_FAILED, request.method);
+ }
+
+ // pass the requested ranges on to the request handler
+ handler->SetRequestRanged(IsRequestRanged(request, lastModified));
+ }
+ }
+ // if we got a POST request we need to take care of the POST data
+ else if (request.method == POST)
+ {
+ // as ownership of the connection handler is passed to libmicrohttpd we must not destroy it
+ SetupPostDataProcessing(request, conHandler.get(), handler, con_cls);
+
+ // as ownership of the connection handler has been passed to libmicrohttpd we must not
+ // destroy it
+ conHandler.release();
+
+ return MHD_YES;
+ }
+
+ return HandleRequest(handler);
+ }
+ }
+ // this is a subsequent call to AnswerToConnection for this request
+ else
+ {
+ // again we need to take special care of the POST data
+ if (request.method == POST)
+ {
+ // process additional / remaining POST data
+ if (ProcessPostData(request, conHandler.get(), upload_data, upload_data_size, con_cls))
+ {
+ // as ownership of the connection handler has been passed to libmicrohttpd we must not
+ // destroy it
+ conHandler.release();
+
+ return MHD_YES;
+ }
+
+ // finalize POST data processing
+ FinalizePostDataProcessing(conHandler.get());
+
+ // check if something went wrong while handling the POST data
+ if (conHandler->errorStatus != MHD_HTTP_OK)
+ return SendErrorResponse(request, conHandler->errorStatus, request.method);
+
+ // we have handled all POST data so it's time to invoke the IHTTPRequestHandler
+ return HandleRequest(conHandler->requestHandler);
+ }
+
+ // it's unusual to get more than one call to AnswerToConnection for none-POST requests, but
+ // let's handle it anyway
+ auto requestHandler = FindRequestHandler(request);
+ if (requestHandler != nullptr)
+ return HandleRequest(requestHandler);
+ }
+
+ m_logger->error("couldn't find any request handler for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+}
+
+MHD_RESULT CWebServer::HandlePostField(void* cls,
+ enum MHD_ValueKind kind,
+ const char* key,
+ const char* filename,
+ const char* content_type,
+ const char* transfer_encoding,
+ const char* data,
+ uint64_t off,
+ size_t size)
+{
+ ConnectionHandler* conHandler = (ConnectionHandler*)cls;
+
+ if (conHandler == nullptr || conHandler->requestHandler == nullptr || key == nullptr ||
+ data == nullptr || size == 0)
+ {
+ GetLogger()->error("unable to handle HTTP POST field");
+ return MHD_NO;
+ }
+
+ conHandler->requestHandler->AddPostField(key, std::string(data, size));
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::HandleRequest(const std::shared_ptr<IHTTPRequestHandler>& handler)
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ HTTPRequest request = handler->GetRequest();
+ MHD_RESULT ret = handler->HandleRequest();
+ if (ret == MHD_NO)
+ {
+ m_logger->error("failed to handle HTTP request for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ struct MHD_Response* response = nullptr;
+ switch (responseDetails.type)
+ {
+ case HTTPNone:
+ m_logger->error("HTTP request handler didn't process {}", request.pathUrl);
+ return MHD_NO;
+
+ case HTTPRedirect:
+ ret = CreateRedirect(request.connection, handler->GetRedirectUrl(), response);
+ break;
+
+ case HTTPFileDownload:
+ ret = CreateFileDownloadResponse(handler, response);
+ break;
+
+ case HTTPMemoryDownloadNoFreeNoCopy:
+ case HTTPMemoryDownloadNoFreeCopy:
+ case HTTPMemoryDownloadFreeNoCopy:
+ case HTTPMemoryDownloadFreeCopy:
+ ret = CreateMemoryDownloadResponse(handler, response);
+ break;
+
+ case HTTPError:
+ ret =
+ CreateErrorResponse(request.connection, responseDetails.status, request.method, response);
+ break;
+
+ default:
+ m_logger->error("internal error while HTTP request handler processed {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ if (ret == MHD_NO)
+ {
+ m_logger->error("failed to create HTTP response for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ return FinalizeRequest(handler, responseDetails.status, response);
+}
+
+MHD_RESULT CWebServer::FinalizeRequest(const std::shared_ptr<IHTTPRequestHandler>& handler,
+ int responseStatus,
+ struct MHD_Response* response)
+{
+ if (handler == nullptr || response == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+
+ // if the request handler has set a content type and it hasn't been set as a header, add it
+ if (!responseDetails.contentType.empty())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, responseDetails.contentType);
+
+ // if the request handler has set a last modified date and it hasn't been set as a header, add it
+ CDateTime lastModified;
+ if (handler->GetLastModifiedDate(lastModified) && lastModified.IsValid())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_LAST_MODIFIED, lastModified.GetAsRFC1123DateTime());
+
+ // check if the request handler has set Cache-Control and add it if not
+ if (!handler->HasResponseHeader(MHD_HTTP_HEADER_CACHE_CONTROL))
+ {
+ int maxAge = handler->GetMaximumAgeForCaching();
+ if (handler->CanBeCached() && maxAge == 0 && !responseDetails.contentType.empty())
+ {
+ // don't cache HTML, CSS and JavaScript files
+ if (!StringUtils::EqualsNoCase(responseDetails.contentType, "text/html") &&
+ !StringUtils::EqualsNoCase(responseDetails.contentType, "text/css") &&
+ !StringUtils::EqualsNoCase(responseDetails.contentType, "application/javascript"))
+ maxAge = CDateTimeSpan(365, 0, 0, 0).GetSecondsTotal();
+ }
+
+ // if the response can't be cached or the maximum age is 0 force the client not to cache
+ if (!handler->CanBeCached() || maxAge == 0)
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CACHE_CONTROL,
+ "private, max-age=0, " HEADER_VALUE_NO_CACHE);
+ else
+ {
+ // create the value of the Cache-Control header
+ std::string cacheControl = StringUtils::Format("public, max-age={}", maxAge);
+
+ // check if the response contains a Set-Cookie header because they must not be cached
+ if (handler->HasResponseHeader(MHD_HTTP_HEADER_SET_COOKIE))
+ cacheControl += ", no-cache=\"set-cookie\"";
+
+ // set the Cache-Control header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CACHE_CONTROL, cacheControl);
+
+ // set the Expires header
+ CDateTime expiryTime = CDateTime::GetCurrentDateTime() + CDateTimeSpan(0, 0, 0, maxAge);
+ handler->AddResponseHeader(MHD_HTTP_HEADER_EXPIRES, expiryTime.GetAsRFC1123DateTime());
+ }
+ }
+
+ // if the request handler can handle ranges and it hasn't been set as a header, add it
+ if (handler->CanHandleRanges())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
+ else
+ handler->AddResponseHeader(MHD_HTTP_HEADER_ACCEPT_RANGES, "none");
+
+ // add all headers set by the request handler
+ for (const auto& it : responseDetails.headers)
+ AddHeader(response, it.first, it.second);
+
+ return SendResponse(request, responseStatus, response);
+}
+
+std::shared_ptr<IHTTPRequestHandler> CWebServer::FindRequestHandler(
+ const HTTPRequest& request) const
+{
+ // look for a IHTTPRequestHandler which can take care of the current request
+ auto requestHandlerIt = std::find_if(m_requestHandlers.cbegin(), m_requestHandlers.cend(),
+ [&request](const IHTTPRequestHandler* requestHandler) {
+ return requestHandler->CanHandleRequest(request);
+ });
+
+ // we found a matching IHTTPRequestHandler so let's get a new instance for this request
+ if (requestHandlerIt != m_requestHandlers.cend())
+ return std::shared_ptr<IHTTPRequestHandler>((*requestHandlerIt)->Create(request));
+
+ return nullptr;
+}
+
+bool CWebServer::IsRequestCacheable(const HTTPRequest& request) const
+{
+ // handle Cache-Control
+ std::string cacheControl = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CACHE_CONTROL);
+ if (!cacheControl.empty())
+ {
+ std::vector<std::string> cacheControls = StringUtils::Split(cacheControl, ",");
+ for (auto control : cacheControls)
+ {
+ control = StringUtils::Trim(control);
+
+ // handle no-cache
+ if (control.compare(HEADER_VALUE_NO_CACHE) == 0)
+ return false;
+ }
+ }
+
+ // handle Pragma
+ std::string pragma = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_PRAGMA);
+ if (pragma.compare(HEADER_VALUE_NO_CACHE) == 0)
+ return false;
+
+ return true;
+}
+
+bool CWebServer::IsRequestRanged(const HTTPRequest& request, const CDateTime& lastModified) const
+{
+ // parse the Range header and store it in the request object
+ CHttpRanges ranges;
+ bool ranged = ranges.Parse(HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE));
+
+ // handle If-Range header but only if the Range header is present
+ if (ranged && lastModified.IsValid())
+ {
+ std::string ifRange = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_RANGE);
+ if (!ifRange.empty() && lastModified.IsValid())
+ {
+ CDateTime ifRangeDate;
+ ifRangeDate.SetFromRFC1123DateTime(ifRange);
+
+ // check if the last modification is newer than the If-Range date
+ // if so we have to server the whole file instead
+ if (lastModified.GetAsUTCDateTime() > ifRangeDate)
+ ranges.Clear();
+ }
+ }
+
+ return !ranges.IsEmpty();
+}
+
+void CWebServer::SetupPostDataProcessing(const HTTPRequest& request,
+ ConnectionHandler* connectionHandler,
+ std::shared_ptr<IHTTPRequestHandler> handler,
+ void** con_cls) const
+{
+ connectionHandler->requestHandler = std::move(handler);
+
+ // we might need to handle the POST data ourselves which is done in the next call to
+ // AnswerToConnection
+ *con_cls = connectionHandler;
+
+ // get the content-type of the POST data
+ const auto contentType = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
+ if (contentType.empty())
+ return;
+
+ // if the content-type is neither application/x-ww-form-urlencoded nor multipart/form-data we need
+ // to handle it ourselves
+ if (!StringUtils::EqualsNoCase(contentType, MHD_HTTP_POST_ENCODING_FORM_URLENCODED) &&
+ !StringUtils::EqualsNoCase(contentType, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA))
+ return;
+
+ // otherwise we can use MHD's POST processor
+ connectionHandler->postprocessor = MHD_create_post_processor(
+ request.connection, MAX_POST_BUFFER_SIZE, &CWebServer::HandlePostField,
+ static_cast<void*>(connectionHandler));
+
+ // MHD doesn't seem to be able to handle this post request
+ if (connectionHandler->postprocessor == nullptr)
+ {
+ m_logger->error("unable to create HTTP POST processor for {}", request.pathUrl);
+ connectionHandler->errorStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+}
+
+bool CWebServer::ProcessPostData(const HTTPRequest& request,
+ ConnectionHandler* connectionHandler,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls) const
+{
+ if (connectionHandler->requestHandler == nullptr)
+ {
+ m_logger->error("cannot handle partial HTTP POST for {} request because there is no valid "
+ "request handler available",
+ request.pathUrl);
+ connectionHandler->errorStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ // we only need to handle POST data if there actually is data left to handle
+ if (*upload_data_size == 0)
+ return false;
+
+ // we may need to handle more POST data which is done in the next call to AnswerToConnection
+ *con_cls = connectionHandler;
+
+ // if nothing has gone wrong so far, process the given POST data
+ if (connectionHandler->errorStatus == MHD_HTTP_OK)
+ {
+ bool postDataHandled = false;
+ // either use MHD's POST processor
+ if (connectionHandler->postprocessor != nullptr)
+ postDataHandled = MHD_post_process(connectionHandler->postprocessor, upload_data,
+ *upload_data_size) == MHD_YES;
+ // or simply copy the data to the handler
+ else if (connectionHandler->requestHandler != nullptr)
+ postDataHandled =
+ connectionHandler->requestHandler->AddPostData(upload_data, *upload_data_size);
+
+ // abort if the received POST data couldn't be handled
+ if (!postDataHandled)
+ {
+ m_logger->error("failed to handle HTTP POST data for {}", request.pathUrl);
+#if (MHD_VERSION >= 0x00097400)
+ connectionHandler->errorStatus = MHD_HTTP_CONTENT_TOO_LARGE;
+#elif (MHD_VERSION >= 0x00095213)
+ connectionHandler->errorStatus = MHD_HTTP_PAYLOAD_TOO_LARGE;
+#else
+ connectionHandler->errorStatus = MHD_HTTP_REQUEST_ENTITY_TOO_LARGE;
+#endif
+ }
+ }
+
+ // signal that we have handled the data
+ *upload_data_size = 0;
+
+ return true;
+}
+
+void CWebServer::FinalizePostDataProcessing(ConnectionHandler* connectionHandler) const
+{
+ if (connectionHandler->postprocessor == nullptr)
+ return;
+
+ MHD_destroy_post_processor(connectionHandler->postprocessor);
+}
+
+MHD_RESULT CWebServer::CreateMemoryDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ // check if the response is completely empty
+ if (responseRanges.empty())
+ return CreateMemoryDownloadResponse(request.connection, nullptr, 0, false, false, response);
+
+ // check if the response contains more ranges than the request asked for
+ if ((request.ranges.IsEmpty() && responseRanges.size() > 1) ||
+ (!request.ranges.IsEmpty() && responseRanges.size() > request.ranges.Size()))
+ {
+ m_logger->warn("response contains more ranges ({}) than the request asked for ({})",
+ static_cast<int>(responseRanges.size()),
+ static_cast<int>(request.ranges.Size()));
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ // if the request asked for no or only one range we can simply use MHDs memory download handler
+ // we MUST NOT send a multipart response
+ if (request.ranges.Size() <= 1)
+ {
+ CHttpResponseRange responseRange = responseRanges.front();
+ // check if the range is valid
+ if (!responseRange.IsValid())
+ {
+ m_logger->warn("invalid response data with range start at {} and end at {}",
+ responseRange.GetFirstPosition(), responseRange.GetLastPosition());
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ const void* responseData = responseRange.GetData();
+ size_t responseDataLength = static_cast<size_t>(responseRange.GetLength());
+
+ switch (responseDetails.type)
+ {
+ case HTTPMemoryDownloadNoFreeNoCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ false, false, response);
+
+ case HTTPMemoryDownloadNoFreeCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ false, true, response);
+
+ case HTTPMemoryDownloadFreeNoCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ true, false, response);
+
+ case HTTPMemoryDownloadFreeCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ true, true, response);
+
+ default:
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+ }
+
+ return CreateRangedMemoryDownloadResponse(handler, response);
+}
+
+MHD_RESULT CWebServer::CreateRangedMemoryDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ // if there's no or only one range this is not the right place
+ if (responseRanges.size() <= 1)
+ return CreateMemoryDownloadResponse(handler, response);
+
+ // extract all the valid ranges and calculate their total length
+ uint64_t firstRangePosition = 0;
+ HttpResponseRanges ranges;
+ for (const auto& range : responseRanges)
+ {
+ // ignore invalid ranges
+ if (!range.IsValid())
+ continue;
+
+ // determine the first range position
+ if (ranges.empty())
+ firstRangePosition = range.GetFirstPosition();
+
+ ranges.push_back(range);
+ }
+
+ if (ranges.empty())
+ return CreateMemoryDownloadResponse(request.connection, nullptr, 0, false, false, response);
+
+ // determine the last range position
+ uint64_t lastRangePosition = ranges.back().GetLastPosition();
+
+ // adjust the HTTP status of the response
+ handler->SetResponseStatus(MHD_HTTP_PARTIAL_CONTENT);
+ // add Content-Range header
+ handler->AddResponseHeader(
+ MHD_HTTP_HEADER_CONTENT_RANGE,
+ HttpRangeUtils::GenerateContentRangeHeaderValue(firstRangePosition, lastRangePosition,
+ responseDetails.totalLength));
+
+ // generate a multipart boundary
+ std::string multipartBoundary = HttpRangeUtils::GenerateMultipartBoundary();
+ // and the content-type
+ std::string contentType = HttpRangeUtils::GenerateMultipartBoundaryContentType(multipartBoundary);
+
+ // add Content-Type header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, contentType);
+
+ // generate the multipart boundary with the Content-Type header field
+ std::string multipartBoundaryWithHeader =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(multipartBoundary, contentType);
+
+ std::string result;
+ // add all the ranges to the result
+ for (HttpResponseRanges::const_iterator range = ranges.begin(); range != ranges.end(); ++range)
+ {
+ // add a newline before any new multipart boundary
+ if (range != ranges.begin())
+ result += HEADER_NEWLINE;
+
+ // generate and append the multipart boundary with the full header (Content-Type and
+ // Content-Length)
+ result +=
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(multipartBoundaryWithHeader, &*range);
+
+ // and append the data of the range
+ result.append(static_cast<const char*>(range->GetData()),
+ static_cast<size_t>(range->GetLength()));
+
+ // check if we need to free the range data
+ if (responseDetails.type == HTTPMemoryDownloadFreeNoCopy ||
+ responseDetails.type == HTTPMemoryDownloadFreeCopy)
+ free(const_cast<void*>(range->GetData()));
+ }
+
+ result += HttpRangeUtils::GenerateMultipartBoundaryEnd(multipartBoundary);
+
+ // add Content-Length header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_LENGTH,
+ std::to_string(static_cast<uint64_t>(result.size())));
+
+ // finally create the response
+ return CreateMemoryDownloadResponse(request.connection, result.c_str(), result.size(), false,
+ true, response);
+}
+
+MHD_RESULT CWebServer::CreateRedirect(struct MHD_Connection* connection,
+ const std::string& strURL,
+ struct MHD_Response*& response) const
+{
+ response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create HTTP redirect response to {}", strURL);
+ return MHD_NO;
+ }
+
+ AddHeader(response, MHD_HTTP_HEADER_LOCATION, strURL);
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateFileDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ std::shared_ptr<XFILE::CFile> file = std::make_shared<XFILE::CFile>();
+ std::string filePath = handler->GetResponseFile();
+
+ // access check
+ if (!CFileUtils::CheckFileAccessAllowed(filePath))
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+
+ if (!file->Open(filePath, XFILE::READ_NO_CACHE))
+ {
+ m_logger->error("Failed to open {}", filePath);
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+ }
+
+ bool ranged = false;
+ uint64_t fileLength = static_cast<uint64_t>(file->GetLength());
+
+ // get the MIME type for the Content-Type header
+ std::string mimeType = responseDetails.contentType;
+ if (mimeType.empty())
+ {
+ std::string ext = URIUtils::GetExtension(filePath);
+ StringUtils::ToLower(ext);
+ mimeType = CreateMimeTypeFromExtension(ext.c_str());
+ }
+
+ uint64_t totalLength = 0;
+ std::unique_ptr<HttpFileDownloadContext> context = std::make_unique<HttpFileDownloadContext>();
+ context->file = file;
+ context->contentType = mimeType;
+ context->boundaryWritten = false;
+ context->writePosition = 0;
+
+ if (handler->IsRequestRanged())
+ {
+ if (!request.ranges.IsEmpty())
+ context->ranges = request.ranges;
+ else
+ HTTPRequestHandlerUtils::GetRequestedRanges(request.connection, fileLength, context->ranges);
+ }
+
+ uint64_t firstPosition = 0;
+ uint64_t lastPosition = 0;
+ // if there are no ranges, add the whole range
+ if (context->ranges.IsEmpty())
+ context->ranges.Add(CHttpRange(0, fileLength - 1));
+ else
+ {
+ handler->SetResponseStatus(MHD_HTTP_PARTIAL_CONTENT);
+
+ // we need to remember that we are ranged because the range length might change and won't be
+ // reliable anymore for length comparisons
+ ranged = true;
+
+ context->ranges.GetFirstPosition(firstPosition);
+ context->ranges.GetLastPosition(lastPosition);
+ }
+
+ // remember the total number of ranges
+ context->rangeCountTotal = context->ranges.Size();
+ // remember the total length
+ totalLength = context->ranges.GetLength();
+
+ // adjust the MIME type and range length in case of multiple ranges which requires multipart
+ // boundaries
+ if (context->rangeCountTotal > 1)
+ {
+ context->boundary = HttpRangeUtils::GenerateMultipartBoundary();
+ mimeType = HttpRangeUtils::GenerateMultipartBoundaryContentType(context->boundary);
+
+ // build part of the boundary with the optional Content-Type header
+ // "--<boundary>\r\nContent-Type: <content-type>\r\n
+ context->boundaryWithHeader = HttpRangeUtils::GenerateMultipartBoundaryWithHeader(
+ context->boundary, context->contentType);
+ context->boundaryEnd = HttpRangeUtils::GenerateMultipartBoundaryEnd(context->boundary);
+
+ // for every range, we need to add a boundary with header
+ for (HttpRanges::const_iterator range = context->ranges.Begin(); range != context->ranges.End();
+ ++range)
+ {
+ // we need to temporarily add the Content-Range header to the boundary to be able to
+ // determine the length
+ std::string completeBoundaryWithHeader =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(context->boundaryWithHeader, &*range);
+ totalLength += completeBoundaryWithHeader.size();
+
+ // add a newline before any new multipart boundary
+ if (range != context->ranges.Begin())
+ totalLength += strlen(HEADER_NEWLINE);
+ }
+ // and at the very end a special end-boundary "\r\n--<boundary>--"
+ totalLength += context->boundaryEnd.size();
+ }
+
+ // set the initial write position
+ context->ranges.GetFirstPosition(context->writePosition);
+
+ // create the response object
+ response =
+ MHD_create_response_from_callback(totalLength, 2048, &CWebServer::ContentReaderCallback,
+ context.get(), &CWebServer::ContentReaderFreeCallback);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP response for {} to be filled from{}", request.pathUrl,
+ filePath);
+ return MHD_NO;
+ }
+
+ context.release(); // ownership was passed to mhd
+
+ // add Content-Range header
+ if (ranged)
+ handler->AddResponseHeader(
+ MHD_HTTP_HEADER_CONTENT_RANGE,
+ HttpRangeUtils::GenerateContentRangeHeaderValue(firstPosition, lastPosition, fileLength));
+
+ // set the Content-Type header
+ if (!mimeType.empty())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, mimeType);
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateErrorResponse(struct MHD_Connection* connection,
+ int responseType,
+ HTTPMethod method,
+ struct MHD_Response*& response) const
+{
+ size_t payloadSize = 0;
+ const void* payload = nullptr;
+
+ switch (responseType)
+ {
+ case MHD_HTTP_NOT_FOUND:
+ payloadSize = strlen(PAGE_FILE_NOT_FOUND);
+ payload = (const void*)PAGE_FILE_NOT_FOUND;
+ break;
+
+ case MHD_HTTP_NOT_IMPLEMENTED:
+ payloadSize = strlen(NOT_SUPPORTED);
+ payload = (const void*)NOT_SUPPORTED;
+ break;
+ }
+
+ response = create_response(payloadSize, payload, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP {} error response", responseType);
+ return MHD_NO;
+ }
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateMemoryDownloadResponse(struct MHD_Connection* connection,
+ const void* data,
+ size_t size,
+ bool free,
+ bool copy,
+ struct MHD_Response*& response) const
+{
+ response = create_response(size, const_cast<void*>(data), free ? MHD_YES : MHD_NO,
+ copy ? MHD_YES : MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP download response");
+ return MHD_NO;
+ }
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::SendResponse(const HTTPRequest& request,
+ int responseStatus,
+ MHD_Response* response) const
+{
+ LogResponse(request, responseStatus);
+
+ MHD_RESULT ret = MHD_queue_response(request.connection, responseStatus, response);
+ MHD_destroy_response(response);
+
+ return ret;
+}
+
+MHD_RESULT CWebServer::SendErrorResponse(const HTTPRequest& request,
+ int errorType,
+ HTTPMethod method) const
+{
+ struct MHD_Response* response = nullptr;
+ MHD_RESULT ret = CreateErrorResponse(request.connection, errorType, method, response);
+ if (ret == MHD_NO)
+ return MHD_NO;
+
+ return SendResponse(request, errorType, response);
+}
+
+void* CWebServer::UriRequestLogger(void* cls, const char* uri)
+{
+ CWebServer* webServer = reinterpret_cast<CWebServer*>(cls);
+
+ // log the full URI
+ if (webServer == nullptr)
+ GetLogger()->debug("request received for {}", uri);
+ else
+ webServer->LogRequest(uri);
+
+ // create and return a new connection handler
+ return new ConnectionHandler(uri);
+}
+
+void CWebServer::LogRequest(const char* uri) const
+{
+ if (uri == nullptr)
+ return;
+
+ m_logger->debug("request received for {}", uri);
+}
+
+ssize_t CWebServer::ContentReaderCallback(void* cls, uint64_t pos, char* buf, size_t max)
+{
+ HttpFileDownloadContext* context = (HttpFileDownloadContext*)cls;
+ if (context == nullptr || context->file == nullptr)
+ return -1;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] write maximum {} bytes from {} ({})", max, context->writePosition,
+ pos);
+
+ // check if we need to add the end-boundary
+ if (context->rangeCountTotal > 1 && context->ranges.IsEmpty())
+ {
+ // put together the end-boundary
+ std::string endBoundary = HttpRangeUtils::GenerateMultipartBoundaryEnd(context->boundary);
+ if ((unsigned int)max != endBoundary.size())
+ return -1;
+
+ // copy the boundary into the buffer
+ memcpy(buf, endBoundary.c_str(), endBoundary.size());
+ return endBoundary.size();
+ }
+
+ CHttpRange range;
+ if (context->ranges.IsEmpty() || !context->ranges.GetFirst(range))
+ return -1;
+
+ uint64_t start = range.GetFirstPosition();
+ uint64_t end = range.GetLastPosition();
+ uint64_t maximum = (uint64_t)max;
+ int written = 0;
+
+ if (context->rangeCountTotal > 1 && !context->boundaryWritten)
+ {
+ // add a newline before any new multipart boundary
+ if (context->rangeCountTotal > context->ranges.Size())
+ {
+ size_t newlineLength = strlen(HEADER_NEWLINE);
+ memcpy(buf, HEADER_NEWLINE, newlineLength);
+ buf += newlineLength;
+ written += newlineLength;
+ maximum -= newlineLength;
+ }
+
+ // put together the boundary for the current range
+ std::string boundary =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(context->boundaryWithHeader, &range);
+
+ // copy the boundary into the buffer
+ memcpy(buf, boundary.c_str(), boundary.size());
+ // advance the buffer position
+ buf += boundary.size();
+ // update the number of written byte
+ written += boundary.size();
+ // update the maximum number of bytes
+ maximum -= boundary.size();
+ context->boundaryWritten = true;
+ }
+
+ // check if the current position is within this range
+ // if not, set it to the start position
+ if (context->writePosition < start || context->writePosition > end)
+ context->writePosition = start;
+ // adjust the maximum number of read bytes
+ maximum = std::min(maximum, end - context->writePosition + 1);
+
+ // seek to the position if necessary
+ if (context->file->GetPosition() < 0 ||
+ context->writePosition != static_cast<uint64_t>(context->file->GetPosition()))
+ context->file->Seek(context->writePosition);
+
+ // read data from the file
+ ssize_t res = context->file->Read(buf, static_cast<size_t>(maximum));
+ if (res <= 0)
+ return -1;
+
+ // add the number of read bytes to the number of written bytes
+ written += res;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] wrote {} bytes from {} in range ({} - {})", written,
+ context->writePosition, start, end);
+
+ // update the current write position
+ context->writePosition += res;
+
+ // if we have read all the data from the current range
+ // remove it from the list
+ if (context->writePosition >= end + 1)
+ {
+ context->ranges.Remove(0);
+ context->boundaryWritten = false;
+ }
+
+ return written;
+}
+
+void CWebServer::ContentReaderFreeCallback(void* cls)
+{
+ HttpFileDownloadContext* context = (HttpFileDownloadContext*)cls;
+ delete context;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] done");
+}
+
+static Logger GetMhdLogger()
+{
+ return CServiceBroker::GetLogging().GetLogger("libmicrohttpd");
+}
+
+// local helper
+static void panicHandlerForMHD(void* unused,
+ const char* file,
+ unsigned int line,
+ const char* reason)
+{
+ GetMhdLogger()->critical("serious error: reason \"{}\" in file \"{}\" at line {}",
+ reason ? reason : "", file ? file : "", line);
+ throw std::runtime_error("MHD serious error"); // FIXME: better solution?
+}
+
+// local helper
+static void logFromMHD(void* unused, const char* fmt, va_list ap)
+{
+ Logger logger = GetMhdLogger();
+ if (fmt == nullptr || fmt[0] == 0)
+ GetMhdLogger()->error("reported error with empty string");
+ else
+ {
+ std::string errDsc = StringUtils::FormatV(fmt, ap);
+ if (errDsc.empty())
+ GetMhdLogger()->error("reported error with unprintable string \"{}\"", fmt);
+ else
+ {
+ if (errDsc.at(errDsc.length() - 1) == '\n')
+ errDsc.erase(errDsc.length() - 1);
+
+ // Most common error is "aborted connection", so log it at LOGDEBUG level
+ GetMhdLogger()->debug(errDsc);
+ }
+ }
+}
+
+bool CWebServer::LoadCert(std::string& skey, std::string& scert)
+{
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+ const char* keyFile = "special://userdata/server.key";
+ const char* certFile = "special://userdata/server.pem";
+
+ if (!file.Exists(keyFile) || !file.Exists(certFile))
+ return false;
+
+ if (file.LoadFile(keyFile, buf) > 0)
+ {
+ skey.resize(buf.size());
+ skey.assign(reinterpret_cast<char*>(buf.data()));
+ file.Close();
+ }
+ else
+ m_logger->error("{}: Error loading: {}", __FUNCTION__, keyFile);
+
+ if (file.LoadFile(certFile, buf) > 0)
+ {
+ scert.resize(buf.size());
+ scert.assign(reinterpret_cast<char*>(buf.data()));
+ file.Close();
+ }
+ else
+ m_logger->error("{}: Error loading: {}", __FUNCTION__, certFile);
+
+ if (!skey.empty() && !scert.empty())
+ {
+ m_logger->info("{}: found server key: {}, certificate: {}, HTTPS support enabled", __FUNCTION__,
+ keyFile, certFile);
+ return true;
+ }
+ return false;
+}
+
+struct MHD_Daemon* CWebServer::StartMHD(unsigned int flags, int port)
+{
+ unsigned int timeout = 60 * 60 * 24;
+ const char* ciphers = "NORMAL:-VERS-TLS1.0";
+
+ MHD_set_panic_func(&panicHandlerForMHD, nullptr);
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_SERVICES_WEBSERVERSSL) &&
+ MHD_is_feature_supported(MHD_FEATURE_SSL) == MHD_YES && LoadCert(m_key, m_cert))
+ // SSL enabled
+ return MHD_start_daemon(
+ flags |
+ // one thread per connection
+ // WARNING: set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
+ // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
+ MHD_USE_THREAD_PER_CONNECTION
+#if (MHD_VERSION >= 0x00095207)
+ |
+ MHD_USE_INTERNAL_POLLING_THREAD /* MHD_USE_THREAD_PER_CONNECTION must be used only with
+ MHD_USE_INTERNAL_POLLING_THREAD since 0.9.54 */
+#endif
+ | MHD_USE_DEBUG /* Print MHD error messages to log */
+ | MHD_USE_SSL,
+ port, 0, 0, &CWebServer::AnswerToConnection, this,
+
+ MHD_OPTION_EXTERNAL_LOGGER, &logFromMHD, 0, MHD_OPTION_CONNECTION_LIMIT, 512,
+ MHD_OPTION_CONNECTION_TIMEOUT, timeout, MHD_OPTION_URI_LOG_CALLBACK,
+ &CWebServer::UriRequestLogger, this, MHD_OPTION_THREAD_STACK_SIZE, m_thread_stacksize,
+ MHD_OPTION_HTTPS_MEM_KEY, m_key.c_str(), MHD_OPTION_HTTPS_MEM_CERT, m_cert.c_str(),
+ MHD_OPTION_HTTPS_PRIORITIES, ciphers, MHD_OPTION_END);
+
+ // No SSL
+ return MHD_start_daemon(
+ flags |
+ // one thread per connection
+ // WARNING: set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
+ // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
+ MHD_USE_THREAD_PER_CONNECTION
+#if (MHD_VERSION >= 0x00095207)
+ | MHD_USE_INTERNAL_POLLING_THREAD /* MHD_USE_THREAD_PER_CONNECTION must be used only with
+ MHD_USE_INTERNAL_POLLING_THREAD since 0.9.54 */
+#endif
+ | MHD_USE_DEBUG /* Print MHD error messages to log */
+ ,
+ port, 0, 0, &CWebServer::AnswerToConnection, this,
+
+ MHD_OPTION_EXTERNAL_LOGGER, &logFromMHD, 0, MHD_OPTION_CONNECTION_LIMIT, 512,
+ MHD_OPTION_CONNECTION_TIMEOUT, timeout, MHD_OPTION_URI_LOG_CALLBACK,
+ &CWebServer::UriRequestLogger, this, MHD_OPTION_THREAD_STACK_SIZE, m_thread_stacksize,
+ MHD_OPTION_END);
+}
+
+bool CWebServer::Start(uint16_t port, const std::string& username, const std::string& password)
+{
+ SetCredentials(username, password);
+ if (!m_running)
+ {
+ // use a new logger containing the port in the name
+ m_logger = CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CWebserver[{}]", port));
+
+ int v6testSock;
+ if ((v6testSock = socket(AF_INET6, SOCK_STREAM, 0)) >= 0)
+ {
+ closesocket(v6testSock);
+ m_daemon_ip6 = StartMHD(MHD_USE_IPv6, port);
+ }
+ m_daemon_ip4 = StartMHD(0, port);
+
+ m_running = (m_daemon_ip6 != nullptr) || (m_daemon_ip4 != nullptr);
+ if (m_running)
+ {
+ m_port = port;
+ m_logger->info("Started");
+ }
+ else
+ m_logger->error("Failed to start");
+ }
+
+ return m_running;
+}
+
+bool CWebServer::Stop()
+{
+ if (!m_running)
+ return true;
+
+ if (m_daemon_ip6 != nullptr)
+ MHD_stop_daemon(m_daemon_ip6);
+
+ if (m_daemon_ip4 != nullptr)
+ MHD_stop_daemon(m_daemon_ip4);
+
+ m_running = false;
+ m_logger->info("Stopped");
+ m_port = 0;
+
+ return true;
+}
+
+bool CWebServer::IsStarted()
+{
+ return m_running;
+}
+
+bool CWebServer::WebServerSupportsSSL()
+{
+ return MHD_is_feature_supported(MHD_FEATURE_SSL) == MHD_YES;
+}
+
+void CWebServer::SetCredentials(const std::string& username, const std::string& password)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_authenticationUsername = username;
+ m_authenticationPassword = password;
+ m_authenticationRequired = !m_authenticationPassword.empty();
+}
+
+void CWebServer::RegisterRequestHandler(IHTTPRequestHandler* handler)
+{
+ if (handler == nullptr)
+ return;
+
+ const auto& it = std::find(m_requestHandlers.cbegin(), m_requestHandlers.cend(), handler);
+ if (it != m_requestHandlers.cend())
+ return;
+
+ m_requestHandlers.push_back(handler);
+ std::sort(m_requestHandlers.begin(), m_requestHandlers.end(),
+ [](IHTTPRequestHandler* lhs, IHTTPRequestHandler* rhs) {
+ return rhs->GetPriority() < lhs->GetPriority();
+ });
+}
+
+void CWebServer::UnregisterRequestHandler(IHTTPRequestHandler* handler)
+{
+ if (handler == nullptr)
+ return;
+
+ m_requestHandlers.erase(std::remove(m_requestHandlers.begin(), m_requestHandlers.end(), handler),
+ m_requestHandlers.end());
+}
+
+void CWebServer::LogRequest(const HTTPRequest& request) const
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ return;
+
+ std::multimap<std::string, std::string> headerValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND,
+ headerValues);
+ std::multimap<std::string, std::string> getValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_GET_ARGUMENT_KIND,
+ getValues);
+
+ m_logger->debug(" [IN] {} {} {}", request.version, GetHTTPMethod(request.method),
+ request.pathUrlFull);
+
+ if (!getValues.empty())
+ {
+ std::vector<std::string> values;
+ for (const auto& get : getValues)
+ values.push_back(get.first + " = " + get.second);
+
+ m_logger->debug(" [IN] Query arguments: {}", StringUtils::Join(values, "; "));
+ }
+
+ for (const auto& header : headerValues)
+ m_logger->debug(" [IN] {}: {}", header.first, header.second);
+}
+
+void CWebServer::LogResponse(const HTTPRequest& request, int responseStatus) const
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ return;
+
+ std::multimap<std::string, std::string> headerValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND,
+ headerValues);
+
+ m_logger->debug("[OUT] {} {} {}", request.version, responseStatus, request.pathUrlFull);
+
+ for (const auto& header : headerValues)
+ m_logger->debug("[OUT] {}: {}", header.first, header.second);
+}
+
+std::string CWebServer::CreateMimeTypeFromExtension(const char* ext)
+{
+ if (strcmp(ext, ".kar") == 0)
+ return "audio/midi";
+ if (strcmp(ext, ".tbn") == 0)
+ return "image/jpeg";
+
+ return CMime::GetMimeType(ext);
+}
+
+MHD_RESULT CWebServer::AddHeader(struct MHD_Response* response,
+ const std::string& name,
+ const std::string& value) const
+{
+ if (response == nullptr || name.empty())
+ return MHD_NO;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ m_logger->debug("[OUT] {}: {}", name, value);
+
+ if (name == MHD_HTTP_HEADER_CONTENT_LENGTH)
+ m_logger->warn("Attempt to override MHD automatic \"Content-Length\" header");
+
+ return MHD_add_response_header(response, name.c_str(), value.c_str());
+}
+
+Logger CWebServer::GetLogger()
+{
+ static Logger s_logger = CServiceBroker::GetLogging().GetLogger("CWebServer");
+ return s_logger;
+}