/* * 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 #include #include #include #include #if defined(TARGET_POSIX) #include #endif #include #define MAX_POST_BUFFER_SIZE 2048 #define PAGE_FILE_NOT_FOUND \ "File not foundFile not found" #define NOT_SUPPORTED \ "Not SupportedThe method you are trying to use is not " \ "supported by this server" #define HEADER_VALUE_NO_CACHE "no-cache" #define HEADER_NEWLINE "\r\n" typedef struct { std::shared_ptr 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(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_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 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(cls); ConnectionHandler* connectionHandler = reinterpret_cast(*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 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& 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& 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 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((*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 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 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(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& 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(responseRanges.size()), static_cast(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(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& 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(range->GetData()), static_cast(range->GetLength())); // check if we need to free the range data if (responseDetails.type == HTTPMemoryDownloadFreeNoCopy || responseDetails.type == HTTPMemoryDownloadFreeCopy) free(const_cast(range->GetData())); } result += HttpRangeUtils::GenerateMultipartBoundaryEnd(multipartBoundary); // add Content-Length header handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_LENGTH, std::to_string(static_cast(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& 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 file = std::make_shared(); 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(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 context = std::make_unique(); 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 // "--\r\nContent-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----" 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(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(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(context->file->GetPosition())) context->file->Seek(context->writePosition); // read data from the file ssize_t res = context->file->Read(buf, static_cast(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 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(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(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 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 headerValues; HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND, headerValues); std::multimap 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 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 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; }