diff options
Diffstat (limited to 'xbmc/network/test')
-rw-r--r-- | xbmc/network/test/CMakeLists.txt | 5 | ||||
-rw-r--r-- | xbmc/network/test/TestWebServer.cpp | 926 | ||||
-rw-r--r-- | xbmc/network/test/data/webserver/test-ranges.txt | 1 | ||||
-rw-r--r-- | xbmc/network/test/data/webserver/test.html | 1 | ||||
-rw-r--r-- | xbmc/network/test/data/webserver/test.png | bin | 0 -> 634 bytes |
5 files changed, 933 insertions, 0 deletions
diff --git a/xbmc/network/test/CMakeLists.txt b/xbmc/network/test/CMakeLists.txt new file mode 100644 index 0000000..a323d18 --- /dev/null +++ b/xbmc/network/test/CMakeLists.txt @@ -0,0 +1,5 @@ +if(MICROHTTPD_FOUND) + set(SOURCES TestWebServer.cpp) + + core_add_test_library(network_test) +endif() diff --git a/xbmc/network/test/TestWebServer.cpp b/xbmc/network/test/TestWebServer.cpp new file mode 100644 index 0000000..aa728ec --- /dev/null +++ b/xbmc/network/test/TestWebServer.cpp @@ -0,0 +1,926 @@ +/* + * 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. + */ + +#if defined(TARGET_WINDOWS) +# include <windows.h> +#endif + +#include <errno.h> +#include <stdlib.h> + +#include <gtest/gtest.h> +#include "URL.h" +#include "filesystem/CurlFile.h" +#include "filesystem/File.h" +#include "interfaces/json-rpc/JSONRPC.h" +#include "network/WebServer.h" +#include "network/httprequesthandler/HTTPVfsHandler.h" +#include "network/httprequesthandler/HTTPJsonRpcHandler.h" +#include "settings/MediaSourceSettings.h" +#include "test/TestUtils.h" +#include "utils/JSONVariantParser.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" + +#include <random> + +using namespace XFILE; + +#define WEBSERVER_HOST "localhost" + +#define TEST_URL_JSONRPC "jsonrpc" + +#define TEST_FILES_DATA "test" +#define TEST_FILES_DATA_RANGES "range1;range2;range3" +#define TEST_FILES_HTML TEST_FILES_DATA ".html" +#define TEST_FILES_RANGES TEST_FILES_DATA "-ranges.txt" + +class TestWebServer : public testing::Test +{ +protected: + TestWebServer() + : webserver(), + sourcePath(XBMC_REF_FILE_PATH("xbmc/network/test/data/webserver/")) + { + static uint16_t port; + if (port == 0) + { + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution<uint16_t> dist(49152, 65535); + port = dist(mt); + } + webserverPort = port; + baseUrl = StringUtils::Format("http://" WEBSERVER_HOST ":{}", webserverPort); + } + ~TestWebServer() override = default; + +protected: + void SetUp() override + { + SetupMediaSources(); + + webserver.Start(webserverPort, "", ""); + webserver.RegisterRequestHandler(&m_jsonRpcHandler); + webserver.RegisterRequestHandler(&m_vfsHandler); + } + + void TearDown() override + { + if (webserver.IsStarted()) + webserver.Stop(); + + webserver.UnregisterRequestHandler(&m_vfsHandler); + webserver.UnregisterRequestHandler(&m_jsonRpcHandler); + + TearDownMediaSources(); + } + + void SetupMediaSources() + { + CMediaSource source; + source.strName = "WebServer Share"; + source.strPath = sourcePath; + source.vecPaths.push_back(sourcePath); + source.m_allowSharing = true; + source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + source.m_iLockMode = LOCK_MODE_EVERYONE; + source.m_ignore = true; + + CMediaSourceSettings::GetInstance().AddShare("videos", source); + } + + void TearDownMediaSources() + { + CMediaSourceSettings::GetInstance().Clear(); + } + + std::string GetUrl(const std::string& path) + { + if (path.empty()) + return baseUrl; + + return URIUtils::AddFileToFolder(baseUrl, path); + } + + std::string GetUrlOfTestFile(const std::string& testFile) + { + if (testFile.empty()) + return ""; + + std::string path = URIUtils::AddFileToFolder(sourcePath, testFile); + path = CURL::Encode(path); + path = URIUtils::AddFileToFolder("vfs", path); + + return GetUrl(path); + } + + bool GetLastModifiedOfTestFile(const std::string& testFile, CDateTime& lastModified) + { + CFile file; + if (!file.Open(URIUtils::AddFileToFolder(sourcePath, testFile), READ_NO_CACHE)) + return false; + + struct __stat64 statBuffer; + if (file.Stat(&statBuffer) != 0) + return false; + + 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 false; + + lastModified = *time; + return lastModified.IsValid(); + } + + void CheckHtmlTestFileResponse(const CCurlFile& curl) + { + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Content-Type must be "text/html" + EXPECT_STREQ("text/html", httpHeader.GetMimeType().c_str()); + // Must be only one "Content-Length" header + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + // Content-Length must be "4" + EXPECT_STREQ("4", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str()); + // Accept-Ranges must be "bytes" + EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // check Last-Modified + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_HTML, lastModified)); + ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos); + EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos); + } + + void CheckRangesTestFileResponse(const CCurlFile& curl, int httpStatus = MHD_HTTP_OK, bool empty = false) + { + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Only zero or one "Content-Length" headers + ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + + // check the protocol line for the expected HTTP status + std::string httpStatusString = StringUtils::Format(" {} ", httpStatus); + std::string protocolLine = httpHeader.GetProtoLine(); + ASSERT_TRUE(protocolLine.find(httpStatusString) != std::string::npos); + + // Content-Type must be "text/html" + EXPECT_STREQ("text/plain", httpHeader.GetMimeType().c_str()); + // check Content-Length + if (!empty) + { + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + EXPECT_STREQ("20", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str()); + } + // Accept-Ranges must be "bytes" + EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // check Last-Modified + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=31536000") != std::string::npos); + EXPECT_TRUE(cacheControl.find("public") != std::string::npos); + } + + void CheckRangesTestFileResponse(const CCurlFile& curl, const std::string& result, const CHttpRanges& ranges) + { + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Only zero or one "Content-Length" headers + ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + + // check the protocol line for the expected HTTP status + std::string httpStatusString = StringUtils::Format(" {} ", MHD_HTTP_PARTIAL_CONTENT); + std::string protocolLine = httpHeader.GetProtoLine(); + ASSERT_TRUE(protocolLine.find(httpStatusString) != std::string::npos); + + // Accept-Ranges must be "bytes" + EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // check Last-Modified + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=31536000") != std::string::npos); + EXPECT_TRUE(cacheControl.find("public") != std::string::npos); + + // If there's no range Content-Length must be "20" + if (ranges.IsEmpty()) + { + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + EXPECT_STREQ("20", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str()); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + return; + } + + // check Content-Range + uint64_t firstPosition, lastPosition; + ASSERT_TRUE(ranges.GetFirstPosition(firstPosition)); + ASSERT_TRUE(ranges.GetLastPosition(lastPosition)); + EXPECT_STREQ(HttpRangeUtils::GenerateContentRangeHeaderValue(firstPosition, lastPosition, 20).c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_RANGE).c_str()); + + std::string expectedContent = TEST_FILES_DATA_RANGES; + const std::string expectedContentType = "text/plain"; + if (ranges.Size() == 1) + { + // Content-Type must be "text/html" + EXPECT_STREQ(expectedContentType.c_str(), httpHeader.GetMimeType().c_str()); + + // check the content + CHttpRange firstRange; + ASSERT_TRUE(ranges.GetFirst(firstRange)); + expectedContent = expectedContent.substr(static_cast<size_t>(firstRange.GetFirstPosition()), static_cast<size_t>(firstRange.GetLength())); + EXPECT_STREQ(expectedContent.c_str(), result.c_str()); + + // and Content-Length + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + EXPECT_STREQ(std::to_string(static_cast<unsigned int>(expectedContent.size())).c_str(), + httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str()); + + return; + } + + // Content-Type contains the multipart boundary + const std::string expectedMimeType = "multipart/byteranges"; + std::string mimeType = httpHeader.GetMimeType(); + ASSERT_STREQ(expectedMimeType.c_str(), mimeType.c_str()); + + std::string contentType = httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_TYPE); + std::string contentTypeStart = expectedMimeType + "; boundary="; + // it must start with "multipart/byteranges; boundary=" followed by the boundary + ASSERT_EQ(0U, contentType.find(contentTypeStart)); + ASSERT_GT(contentType.size(), contentTypeStart.size()); + // extract the boundary + std::string multipartBoundary = contentType.substr(contentTypeStart.size()); + ASSERT_FALSE(multipartBoundary.empty()); + multipartBoundary = "--" + multipartBoundary; + + ASSERT_EQ(0U, result.find(multipartBoundary)); + std::vector<std::string> rangeParts = StringUtils::Split(result, multipartBoundary); + // the first part is not really a part and is therefore empty (the place before the first boundary) + ASSERT_TRUE(rangeParts.front().empty()); + rangeParts.erase(rangeParts.begin()); + // the last part is the end of the end multipart boundary + ASSERT_STREQ("--", rangeParts.back().c_str()); + rangeParts.erase(rangeParts.begin() + rangeParts.size() - 1); + ASSERT_EQ(ranges.Size(), rangeParts.size()); + + for (size_t i = 0; i < rangeParts.size(); ++i) + { + std::string data = rangeParts.at(i); + StringUtils::Trim(data, " \r\n"); + + // find the separator between header and data + size_t pos = data.find("\r\n\r\n"); + ASSERT_NE(std::string::npos, pos); + + std::string header = data.substr(0, pos + 4); + data = data.substr(pos + 4); + + // get the expected range + CHttpRange range; + ASSERT_TRUE(ranges.Get(i, range)); + + // parse the header of the range part + CHttpHeader rangeHeader; + rangeHeader.Parse(header); + + // check Content-Type + EXPECT_STREQ(expectedContentType.c_str(), rangeHeader.GetMimeType().c_str()); + + // parse and check Content-Range + std::string contentRangeHeader = rangeHeader.GetValue(MHD_HTTP_HEADER_CONTENT_RANGE); + std::vector<std::string> contentRangeHeaderParts = StringUtils::Split(contentRangeHeader, "/"); + ASSERT_EQ(2U, contentRangeHeaderParts.size()); + + // check the length of the range + EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeHeaderParts.back())); + uint64_t contentRangeLength = str2uint64(contentRangeHeaderParts.back()); + EXPECT_EQ(range.GetLength(), contentRangeLength); + + // remove the leading "bytes " string from the range definition + std::string contentRangeDefinition = contentRangeHeaderParts.front(); + ASSERT_EQ(0U, contentRangeDefinition.find("bytes ")); + contentRangeDefinition = contentRangeDefinition.substr(6); + + // check the start and end positions of the range + std::vector<std::string> contentRangeParts = StringUtils::Split(contentRangeDefinition, "-"); + ASSERT_EQ(2U, contentRangeParts.size()); + EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeParts.front())); + uint64_t contentRangeStart = str2uint64(contentRangeParts.front()); + EXPECT_EQ(range.GetFirstPosition(), contentRangeStart); + EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeParts.back())); + uint64_t contentRangeEnd = str2uint64(contentRangeParts.back()); + EXPECT_EQ(range.GetLastPosition(), contentRangeEnd); + + // make sure the length of the content matches the one of the expected range + EXPECT_EQ(range.GetLength(), data.size()); + EXPECT_STREQ(expectedContent.substr(static_cast<size_t>(range.GetFirstPosition()), static_cast<size_t>(range.GetLength())).c_str(), data.c_str()); + } + } + + std::string GenerateRangeHeaderValue(unsigned int start, unsigned int end) + { + return StringUtils::Format("bytes={}-{}", start, end); + } + + CWebServer webserver; + CHTTPJsonRpcHandler m_jsonRpcHandler; + CHTTPVfsHandler m_vfsHandler; + std::string baseUrl; + std::string sourcePath; + uint16_t webserverPort; +}; + +TEST_F(TestWebServer, IsStarted) +{ + ASSERT_TRUE(webserver.IsStarted()); +} + +TEST_F(TestWebServer, CanGetJsonRpcApiDescriptionWithHttpGet) +{ + std::string result; + CCurlFile curl; + ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC), result)); + ASSERT_FALSE(result.empty()); + + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Content-Length header must be present + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + // Content-Type must be "application/json" + EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str()); + // Accept-Ranges must be "none" + EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos); + EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos); +} + +TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpGet) +{ + // initialized JSON-RPC + JSONRPC::CJSONRPC::Initialize(); + + std::string result; + CCurlFile curl; + ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }")), result)); + ASSERT_FALSE(result.empty()); + + // parse the JSON-RPC response + CVariant resultObj; + ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj)); + // make sure it's an object + ASSERT_TRUE(resultObj.isObject()); + + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Content-Length header must be present + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + // Content-Type must be "application/json" + EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str()); + // Accept-Ranges must be "none" + EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos); + EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos); + + // uninitialize JSON-RPC + JSONRPC::CJSONRPC::Cleanup(); +} + +TEST_F(TestWebServer, CannotModifyOverJsonRpcWithHttpGet) +{ + // initialized JSON-RPC + JSONRPC::CJSONRPC::Initialize(); + + std::string result; + CCurlFile curl; + ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }")), result)); + ASSERT_FALSE(result.empty()); + + // parse the JSON-RPC response + CVariant resultObj; + ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj)); + // make sure it's an object + ASSERT_TRUE(resultObj.isObject()); + // it must contain the "error" property with the "Bad client permission" error code + ASSERT_TRUE(resultObj.isMember("error") && resultObj["error"].isObject()); + ASSERT_TRUE(resultObj["error"].isMember("code") && resultObj["error"]["code"].isInteger()); + ASSERT_EQ(JSONRPC::BadPermission, resultObj["error"]["code"].asInteger()); + + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Content-Length header must be present + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + // Content-Type must be "application/json" + EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str()); + // Accept-Ranges must be "none" + EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos); + EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos); + + // uninitialize JSON-RPC + JSONRPC::CJSONRPC::Cleanup(); +} + +TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpPost) +{ + // initialized JSON-RPC + JSONRPC::CJSONRPC::Initialize(); + + std::string result; + CCurlFile curl; + curl.SetMimeType("application/json"); + ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }", result)); + ASSERT_FALSE(result.empty()); + + // parse the JSON-RPC response + CVariant resultObj; + ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj)); + // make sure it's an object + ASSERT_TRUE(resultObj.isObject()); + + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Content-Length header must be present + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + // Content-Type must be "application/json" + EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str()); + // Accept-Ranges must be "none" + EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos); + EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos); + + // uninitialize JSON-RPC + JSONRPC::CJSONRPC::Cleanup(); +} + +TEST_F(TestWebServer, CanModifyOverJsonRpcWithHttpPost) +{ + // initialized JSON-RPC + JSONRPC::CJSONRPC::Initialize(); + + std::string result; + CCurlFile curl; + curl.SetMimeType("application/json"); + ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }", result)); + ASSERT_FALSE(result.empty()); + + // parse the JSON-RPC response + CVariant resultObj; + ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj)); + // make sure it's an object + ASSERT_TRUE(resultObj.isObject()); + // it must contain the "result" property with the "OK" value + ASSERT_TRUE(resultObj.isMember("result") && resultObj["result"].isString()); + EXPECT_STREQ("OK", resultObj["result"].asString().c_str()); + + // get the HTTP header details + const CHttpHeader& httpHeader = curl.GetHttpHeader(); + + // Content-Length header must be present + ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size()); + // Content-Type must be "application/json" + EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str()); + // Accept-Ranges must be "none" + EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str()); + + // Cache-Control must contain "mag-age=0" and "no-cache" + std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL); + EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos); + EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos); + + // uninitialize JSON-RPC + JSONRPC::CJSONRPC::Cleanup(); +} + +TEST_F(TestWebServer, CanNotHeadNonExistingFile) +{ + CCurlFile curl; + ASSERT_FALSE(curl.Exists(CURL(GetUrlOfTestFile("file_does_not_exist")))); +} + +TEST_F(TestWebServer, CanHeadFile) +{ + CCurlFile curl; + ASSERT_TRUE(curl.Exists(CURL(GetUrlOfTestFile(TEST_FILES_HTML)))); + + CheckHtmlTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanNotGetNonExistingFile) +{ + std::string result; + CCurlFile curl; + ASSERT_FALSE(curl.Get(GetUrlOfTestFile("file_does_not_exist"), result)); + ASSERT_TRUE(result.empty()); +} + +TEST_F(TestWebServer, CanGetFile) +{ + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_HTML), result)); + ASSERT_STREQ(TEST_FILES_DATA, result.c_str()); + + CheckHtmlTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanGetFileForcingNoCache) +{ + // check non-cacheable HTML with Control-Cache: no-cache + std::string result; + CCurlFile curl_html; + curl_html.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl_html.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache"); + ASSERT_TRUE(curl_html.Get(GetUrlOfTestFile(TEST_FILES_HTML), result)); + EXPECT_STREQ(TEST_FILES_DATA, result.c_str()); + CheckHtmlTestFileResponse(curl_html); + + // check cacheable text file with Control-Cache: no-cache + result.clear(); + CCurlFile curl_txt; + curl_txt.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl_txt.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache"); + ASSERT_TRUE(curl_txt.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl_txt); + + // check cacheable text file with deprecated Pragma: no-cache + result.clear(); + CCurlFile curl_txt_pragma; + curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_PRAGMA, "no-cache"); + ASSERT_TRUE(curl_txt_pragma.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl_txt_pragma); +} + +TEST_F(TestWebServer, CanGetCachedFileWithOlderIfModifiedSince) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0); + + // get the file with an older If-Modified-Since value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedOlder.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanGetCachedFileWithExactIfModifiedSince) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + + // get the file with the exact If-Modified-Since value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModified.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + ASSERT_TRUE(result.empty()); + CheckRangesTestFileResponse(curl, MHD_HTTP_NOT_MODIFIED, true); +} + +TEST_F(TestWebServer, CanGetCachedFileWithNewerIfModifiedSince) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0); + + // get the file with a newer If-Modified-Since value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, + lastModifiedNewer.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + ASSERT_TRUE(result.empty()); + CheckRangesTestFileResponse(curl, MHD_HTTP_NOT_MODIFIED, true); +} + +TEST_F(TestWebServer, CanGetCachedFileWithNewerIfModifiedSinceForcingNoCache) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0); + + // get the file with a newer If-Modified-Since value but forcing no caching + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime()); + curl.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache"); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanGetCachedFileWithOlderIfUnmodifiedSince) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0); + + // get the file with an older If-Unmodified-Since value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedOlder.GetAsRFC1123DateTime()); + ASSERT_FALSE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); +} + +TEST_F(TestWebServer, CanGetCachedFileWithExactIfUnmodifiedSince) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + + // get the file with an older If-Unmodified-Since value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModified.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanGetCachedFileWithNewerIfUnmodifiedSince) +{ + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0); + + // get the file with a newer If-Unmodified-Since value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, ""); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanGetRangedFileRange0_) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + const std::string range = "bytes=0-"; + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify the beginning of the range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRange0_End) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size()); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify the whole range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRange0_2xEnd) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size() * 2); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify a larger range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRange0_First) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";"); + const std::string range = GenerateRangeHeaderValue(0, rangedContent.front().size() - 1); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify a larger range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRangeFirst_Second) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";"); + const std::string range = GenerateRangeHeaderValue(rangedContent.front().size() + 1, rangedContent.front().size() + 1 + rangedContent.at(2).size() - 1); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify a larger range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRange_Last) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";"); + const std::string range = + StringUtils::Format("bytes=-{}", static_cast<unsigned int>(rangedContent.back().size())); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify a larger range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecond) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";"); + const std::string range = StringUtils::Format( + "bytes=0-{},{}-{}", static_cast<unsigned int>(rangedContent.front().size() - 1), + static_cast<unsigned int>(rangedContent.front().size() + 1), + static_cast<unsigned int>(rangedContent.front().size() + 1) + + static_cast<unsigned int>(rangedContent.at(1).size() - 1)); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify a larger range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecondLast) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";"); + const std::string range = StringUtils::Format( + "bytes=0-{},{}-{},-{}", static_cast<unsigned int>(rangedContent.front().size() - 1), + static_cast<unsigned int>(rangedContent.front().size() + 1), + static_cast<unsigned int>(rangedContent.front().size() + 1) + + static_cast<unsigned int>(rangedContent.at(1).size() - 1), + static_cast<unsigned int>(rangedContent.back().size())); + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the whole file but specify a larger range + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetCachedRangedFileWithOlderIfRange) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + const std::string range = "bytes=0-"; + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0); + + // get the whole file (but ranged) with an older If-Range value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModifiedOlder.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str()); + CheckRangesTestFileResponse(curl); +} + +TEST_F(TestWebServer, CanGetCachedRangedFileWithExactIfRange) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + const std::string range = "bytes=0-"; + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + + // get the whole file (but ranged) with an older If-Range value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModified.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} + +TEST_F(TestWebServer, CanGetCachedRangedFileWithNewerIfRange) +{ + const std::string rangedFileContent = TEST_FILES_DATA_RANGES; + const std::string range = "bytes=0-"; + + CHttpRanges ranges; + ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size())); + + // get the last modified date of the file + CDateTime lastModified; + ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified)); + CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0); + + // get the whole file (but ranged) with an older If-Range value + std::string result; + CCurlFile curl; + curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range); + curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModifiedNewer.GetAsRFC1123DateTime()); + ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result)); + CheckRangesTestFileResponse(curl, result, ranges); +} diff --git a/xbmc/network/test/data/webserver/test-ranges.txt b/xbmc/network/test/data/webserver/test-ranges.txt new file mode 100644 index 0000000..6c0a04b --- /dev/null +++ b/xbmc/network/test/data/webserver/test-ranges.txt @@ -0,0 +1 @@ +range1;range2;range3
\ No newline at end of file diff --git a/xbmc/network/test/data/webserver/test.html b/xbmc/network/test/data/webserver/test.html new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/xbmc/network/test/data/webserver/test.html @@ -0,0 +1 @@ +test
\ No newline at end of file diff --git a/xbmc/network/test/data/webserver/test.png b/xbmc/network/test/data/webserver/test.png Binary files differnew file mode 100644 index 0000000..f792601 --- /dev/null +++ b/xbmc/network/test/data/webserver/test.png |