void CheckRangesTestFileResponse(const CCurlFile& curl, int httpStatus = MHD_HTTP_OK, bool empty = false) { // get the HTTP header details const CHttpHeader& httpHeader = curl.GetHttpHeader(); // check the protocol line for the expected HTTP status std::string httpStatusString = StringUtils::Format(" %d ", 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) EXPECT_STREQ("0", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str()); else 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); }
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, 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, 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, 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); }
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()); // 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); }
int CWebServer::FinalizeRequest(IHTTPRequestHandler *handler, int responseStatus, struct MHD_Response *response) { if (handler == NULL || response == NULL) 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=%d", 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 MHD_HTTP_HEADER_CONTENT_LENGTH if (responseDetails.totalLength > 0) handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_LENGTH, StringUtils::Format("%" PRIu64, responseDetails.totalLength)); // add all headers set by the request handler for (std::multimap<std::string, std::string>::const_iterator it = responseDetails.headers.begin(); it != responseDetails.headers.end(); ++it) AddHeader(response, it->first, it->second); #ifdef WEBSERVER_DEBUG std::multimap<std::string, std::string> headerValues; GetRequestHeaderValues(request.connection, MHD_RESPONSE_HEADER_KIND, headerValues); CLog::Log(LOGDEBUG, "webserver [OUT] %s %d %s", request.version.c_str(), responseStatus, request.pathUrlFull.c_str()); for (std::multimap<std::string, std::string>::const_iterator header = headerValues.begin(); header != headerValues.end(); ++header) CLog::Log(LOGDEBUG, "webserver [OUT] %s: %s", header->first.c_str(), header->second.c_str()); #endif int ret = MHD_queue_response(request.connection, responseStatus, response); MHD_destroy_response(response); delete handler; return ret; }
void CheckRangesTestFileResponse(const CCurlFile& curl, const std::string& result, const CHttpRanges& ranges) { // get the HTTP header details const CHttpHeader& httpHeader = curl.GetHttpHeader(); // check the protocol line for the expected HTTP status std::string httpStatusString = StringUtils::Format(" %d ", 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()) { 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(firstRange.GetFirstPosition(), firstRange.GetLength()); EXPECT_STREQ(expectedContent.c_str(), result.c_str()); // and Content-Length EXPECT_STREQ(StringUtils::Format("%u", 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(0, 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(0, 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(2, 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(0, 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(2, 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(range.GetFirstPosition(), range.GetLength()).c_str(), data.c_str()); } }
int CWebServer::CreateFileDownloadResponse(struct MHD_Connection *connection, const string &strURL, HTTPMethod methodType, struct MHD_Response *&response, int &responseCode) { CFile *file = new CFile(); #ifdef WEBSERVER_DEBUG CLog::Log(LOGDEBUG, "webserver [IN] %s", strURL.c_str()); multimap<string, string> headers; if (GetRequestHeaderValues(connection, MHD_HEADER_KIND, headers) > 0) { for (multimap<string, string>::const_iterator header = headers.begin(); header != headers.end(); header++) CLog::Log(LOGDEBUG, "webserver [IN] %s: %s", header->first.c_str(), header->second.c_str()); } #endif if (file->Open(strURL, READ_NO_CACHE)) { bool getData = true; bool ranged = false; int64_t fileLength = file->GetLength(); // try to get the file's last modified date CDateTime lastModified; if (!GetLastModifiedDateTime(file, lastModified)) lastModified.Reset(); // get the MIME type for the Content-Type header CStdString ext = URIUtils::GetExtension(strURL); ext = ext.ToLower(); string mimeType = CreateMimeTypeFromExtension(ext.c_str()); if (methodType != HEAD) { int64_t firstPosition = 0; int64_t lastPosition = fileLength - 1; uint64_t totalLength = 0; HttpFileDownloadContext *context = new HttpFileDownloadContext(); context->file = file; context->rangesLength = fileLength; context->contentType = mimeType; context->boundaryWritten = false; context->writePosition = 0; if (methodType == GET) { // handle If-Modified-Since string ifModifiedSince = GetRequestHeaderValue(connection, MHD_HEADER_KIND, "If-Modified-Since"); if (!ifModifiedSince.empty() && lastModified.IsValid()) { CDateTime ifModifiedSinceDate; ifModifiedSinceDate.SetFromRFC1123DateTime(ifModifiedSince); if (lastModified.GetAsUTCDateTime() <= ifModifiedSinceDate) { getData = false; response = MHD_create_response_from_data(0, NULL, MHD_NO, MHD_NO); responseCode = MHD_HTTP_NOT_MODIFIED; } } if (getData) { // handle Range header context->rangesLength = ParseRangeHeader(GetRequestHeaderValue(connection, MHD_HEADER_KIND, "Range"), fileLength, context->ranges, firstPosition, lastPosition); // handle If-Range header but only if the Range header is present if (!context->ranges.empty()) { string ifRange = GetRequestHeaderValue(connection, MHD_HEADER_KIND, "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) context->ranges.clear(); } } } } if (getData) { // if there are no ranges, add the whole range if (context->ranges.empty() || context->rangesLength == fileLength) { if (context->rangesLength == fileLength) context->ranges.clear(); context->ranges.push_back(HttpRange(0, fileLength - 1)); context->rangesLength = fileLength; firstPosition = 0; lastPosition = fileLength - 1; } else responseCode = MHD_HTTP_PARTIAL_CONTENT; // remember the total number of ranges context->rangeCount = context->ranges.size(); // remember the total length totalLength = context->rangesLength; // we need to remember whether we are ranged because the range length // might change and won't be reliable anymore for length comparisons ranged = context->rangeCount > 1 || context->rangesLength < fileLength; // adjust the MIME type and range length in case of multiple ranges // which requires multipart boundaries if (context->rangeCount > 1) { context->boundary = GenerateMultipartBoundary(); mimeType = "multipart/byteranges; boundary=" + context->boundary; // build part of the boundary with the optional Content-Type header // "--<boundary>\r\nContent-Type: <content-type>\r\n context->boundaryWithHeader = "\r\n--" + context->boundary + "\r\n"; if (!context->contentType.empty()) context->boundaryWithHeader += "Content-Type: " + context->contentType + "\r\n"; // 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 string completeBoundaryWithHeader = context->boundaryWithHeader; completeBoundaryWithHeader += StringUtils::Format("Content-Range: " CONTENT_RANGE_FORMAT, range->first, range->second, range->second - range->first + 1); completeBoundaryWithHeader += "\r\n\r\n"; totalLength += completeBoundaryWithHeader.size(); } // and at the very end a special end-boundary "\r\n--<boundary>--" totalLength += 4 + context->boundary.size() + 2; } // set the initial write position context->writePosition = context->ranges.begin()->first; // create the response object response = MHD_create_response_from_callback(totalLength, 2048, &CWebServer::ContentReaderCallback, context, &CWebServer::ContentReaderFreeCallback); } if (response == NULL) { file->Close(); delete file; delete context; return MHD_NO; } // add Content-Range header if (ranged) AddHeader(response, "Content-Range", StringUtils::Format(CONTENT_RANGE_FORMAT, firstPosition, lastPosition, fileLength).c_str()); } else { getData = false; CStdString contentLength; contentLength.Format("%" PRId64, fileLength); response = MHD_create_response_from_data(0, NULL, MHD_NO, MHD_NO); if (response == NULL) { file->Close(); delete file; return MHD_NO; } AddHeader(response, "Content-Length", contentLength); } // add "Accept-Ranges: bytes" header AddHeader(response, "Accept-Ranges", "bytes"); // set the Content-Type header if (!mimeType.empty()) AddHeader(response, "Content-Type", mimeType.c_str()); // set the Last-Modified header if (lastModified.IsValid()) AddHeader(response, "Last-Modified", lastModified.GetAsRFC1123DateTime()); // set the Expires header CDateTime expiryTime = CDateTime::GetCurrentDateTime(); if (StringUtils::EqualsNoCase(mimeType, "text/html") || StringUtils::EqualsNoCase(mimeType, "text/css") || StringUtils::EqualsNoCase(mimeType, "application/javascript")) expiryTime += CDateTimeSpan(1, 0, 0, 0); else expiryTime += CDateTimeSpan(365, 0, 0, 0); AddHeader(response, "Expires", expiryTime.GetAsRFC1123DateTime()); // only close the CFile instance if libmicrohttpd doesn't have to grab the data of the file if (!getData) { file->Close(); delete file; } } else { delete file; CLog::Log(LOGERROR, "WebServer: Failed to open %s", strURL.c_str()); return SendErrorResponse(connection, MHD_HTTP_NOT_FOUND, methodType); } return MHD_YES; }
int CWebServer::CreateFileDownloadResponse(struct MHD_Connection *connection, const string &strURL, HTTPMethod methodType, struct MHD_Response *&response, int &responseCode) { CFile *file = new CFile(); if (file->Open(strURL, READ_NO_CACHE)) { bool getData = true; if (methodType != HEAD) { if (methodType == GET) { string ifModifiedSince = GetRequestHeaderValue(connection, MHD_HEADER_KIND, "If-Modified-Since"); if (!ifModifiedSince.empty()) { CDateTime ifModifiedSinceDate; ifModifiedSinceDate.SetFromRFC1123DateTime(ifModifiedSince); struct __stat64 statBuffer; if (file->Stat(&statBuffer) == 0) { struct tm *time = localtime((time_t *)&statBuffer.st_mtime); if (time != NULL) { CDateTime lastModified = *time; if (lastModified.GetAsUTCDateTime() <= ifModifiedSinceDate) { getData = false; response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO); responseCode = MHD_HTTP_NOT_MODIFIED; } } } } } if (getData) response = MHD_create_response_from_callback(file->GetLength(), 2048, &CWebServer::ContentReaderCallback, file, &CWebServer::ContentReaderFreeCallback); if (response == NULL) { file->Close(); delete file; return MHD_NO; } } else { getData = false; CStdString contentLength; contentLength.Format("%I64d", file->GetLength()); response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO); if (response == NULL) { file->Close(); delete file; return MHD_NO; } MHD_add_response_header(response, "Content-Length", contentLength); } // set the Content-Type header CStdString ext = URIUtils::GetExtension(strURL); ext = ext.ToLower(); const char *mime = CreateMimeTypeFromExtension(ext.c_str()); if (mime) MHD_add_response_header(response, "Content-Type", mime); // set the Last-Modified header struct __stat64 statBuffer; if (file->Stat(&statBuffer) == 0) { struct tm *time = localtime((time_t *)&statBuffer.st_mtime); if (time != NULL) { CDateTime lastModified = *time; MHD_add_response_header(response, "Last-Modified", lastModified.GetAsRFC1123DateTime()); } } // set the Expires header CDateTime expiryTime = CDateTime::GetCurrentDateTime(); if (mime && strncmp(mime, "text/html", 9) == 0) expiryTime += CDateTimeSpan(1, 0, 0, 0); else expiryTime += CDateTimeSpan(365, 0, 0, 0); MHD_add_response_header(response, "Expires", expiryTime.GetAsRFC1123DateTime()); // only close the CFile instance if libmicrohttpd doesn't have to grab the data of the file if (!getData) { file->Close(); delete file; } } else { delete file; CLog::Log(LOGERROR, "WebServer: Failed to open %s", strURL.c_str()); return SendErrorResponse(connection, MHD_HTTP_NOT_FOUND, GET); /* GET Assumed Temporarily */ } return MHD_YES; }
int CWebServer::CreateFileDownloadResponse(struct MHD_Connection *connection, const string &strURL, HTTPMethod methodType, struct MHD_Response *&response, int &responseCode) { boost::shared_ptr<CFile> file = boost::make_shared<CFile>(); #ifdef WEBSERVER_DEBUG CLog::Log(LOGDEBUG, "webserver [IN] %s", strURL.c_str()); multimap<string, string> headers; if (GetRequestHeaderValues(connection, MHD_HEADER_KIND, headers) > 0) { for (multimap<string, string>::const_iterator header = headers.begin(); header != headers.end(); header++) CLog::Log(LOGDEBUG, "webserver [IN] %s: %s", header->first.c_str(), header->second.c_str()); } #endif if (!file->Open(strURL, READ_NO_CACHE)) { CLog::Log(LOGERROR, "WebServer: Failed to open %s", strURL.c_str()); return SendErrorResponse(connection, MHD_HTTP_NOT_FOUND, methodType); } bool getData = true; bool ranged = false; int64_t fileLength = file->GetLength(); // try to get the file's last modified date CDateTime lastModified; if (!GetLastModifiedDateTime(file.get(), lastModified)) lastModified.Reset(); // get the MIME type for the Content-Type header std::string ext = URIUtils::GetExtension(strURL); StringUtils::ToLower(ext); string mimeType = CreateMimeTypeFromExtension(ext.c_str()); if (methodType != HEAD) { int64_t firstPosition = 0; int64_t lastPosition = fileLength - 1; uint64_t totalLength = 0; std::auto_ptr<HttpFileDownloadContext> context(new HttpFileDownloadContext()); context->file = file; context->rangesLength = fileLength; context->contentType = mimeType; context->boundaryWritten = false; context->writePosition = 0; if (methodType == GET) { bool cacheable = true; // handle Cache-Control string cacheControl = GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CACHE_CONTROL); if (!cacheControl.empty()) { vector<string> cacheControls = StringUtils::Split(cacheControl, ","); for (vector<string>::const_iterator it = cacheControls.begin(); it != cacheControls.end(); ++it) { string control = *it; control = StringUtils::Trim(control); // handle no-cache if (control.compare(HEADER_VALUE_NO_CACHE) == 0) cacheable = false; } } if (cacheable) { // handle Pragma (but only if "Cache-Control: no-cache" hasn't been set) string pragma = GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_PRAGMA); if (pragma.compare(HEADER_VALUE_NO_CACHE) == 0) cacheable = false; } if (lastModified.IsValid()) { // handle If-Modified-Since or If-Unmodified-Since string ifModifiedSince = GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MODIFIED_SINCE); string ifUnmodifiedSince = 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) { getData = false; response = MHD_create_response_from_data(0, NULL, MHD_NO, MHD_NO); if (response == NULL) return MHD_NO; responseCode = MHD_HTTP_NOT_MODIFIED; } // handle If-Unmodified-Since else if (ifUnmodifiedSinceDate.SetFromRFC1123DateTime(ifUnmodifiedSince) && lastModified.GetAsUTCDateTime() > ifUnmodifiedSinceDate) return SendErrorResponse(connection, MHD_HTTP_PRECONDITION_FAILED, methodType); } if (getData) { // handle Range header context->rangesLength = ParseRangeHeader(GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE), fileLength, context->ranges, firstPosition, lastPosition); // handle If-Range header but only if the Range header is present if (!context->ranges.empty()) { string ifRange = GetRequestHeaderValue(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) context->ranges.clear(); } } } } if (getData) { // if there are no ranges, add the whole range if (context->ranges.empty() || context->rangesLength == fileLength) { if (context->rangesLength == fileLength) context->ranges.clear(); context->ranges.push_back(HttpRange(0, fileLength - 1)); context->rangesLength = fileLength; firstPosition = 0; lastPosition = fileLength - 1; } else responseCode = MHD_HTTP_PARTIAL_CONTENT; // remember the total number of ranges context->rangeCount = context->ranges.size(); // remember the total length totalLength = context->rangesLength; // we need to remember whether we are ranged because the range length // might change and won't be reliable anymore for length comparisons ranged = context->rangeCount > 1 || context->rangesLength < fileLength; // adjust the MIME type and range length in case of multiple ranges // which requires multipart boundaries if (context->rangeCount > 1) { context->boundary = GenerateMultipartBoundary(); mimeType = "multipart/byteranges; boundary=" + context->boundary; // build part of the boundary with the optional Content-Type header // "--<boundary>\r\nContent-Type: <content-type>\r\n context->boundaryWithHeader = HEADER_NEWLINE HEADER_BOUNDARY + context->boundary + HEADER_NEWLINE; if (!context->contentType.empty()) context->boundaryWithHeader += MHD_HTTP_HEADER_CONTENT_TYPE ": " + context->contentType + HEADER_NEWLINE; // 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 string completeBoundaryWithHeader = context->boundaryWithHeader; completeBoundaryWithHeader += StringUtils::Format(MHD_HTTP_HEADER_CONTENT_RANGE ": " CONTENT_RANGE_FORMAT, range->first, range->second, range->second - range->first + 1); completeBoundaryWithHeader += HEADER_SEPARATOR; totalLength += completeBoundaryWithHeader.size(); } // and at the very end a special end-boundary "\r\n--<boundary>--" totalLength += strlen(HEADER_SEPARATOR) + strlen(HEADER_BOUNDARY) + context->boundary.size() + strlen(HEADER_BOUNDARY); } // set the initial write position context->writePosition = context->ranges.begin()->first; // create the response object response = MHD_create_response_from_callback(totalLength, 2048, &CWebServer::ContentReaderCallback, context.get(), &CWebServer::ContentReaderFreeCallback); if (response == NULL) return MHD_NO; context.release(); // ownership was passed to mhd } // add Content-Range header if (ranged) AddHeader(response, MHD_HTTP_HEADER_CONTENT_RANGE, StringUtils::Format(CONTENT_RANGE_FORMAT, firstPosition, lastPosition, fileLength)); } else { getData = false; std::string contentLength = StringUtils::Format("%" PRId64, fileLength); response = MHD_create_response_from_data(0, NULL, MHD_NO, MHD_NO); if (response == NULL) return MHD_NO; AddHeader(response, MHD_HTTP_HEADER_CONTENT_LENGTH, contentLength); } // add "Accept-Ranges: bytes" header AddHeader(response, MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes"); // set the Content-Type header if (!mimeType.empty()) AddHeader(response, MHD_HTTP_HEADER_CONTENT_TYPE, mimeType); // set the Last-Modified header if (lastModified.IsValid()) AddHeader(response, MHD_HTTP_HEADER_LAST_MODIFIED, lastModified.GetAsRFC1123DateTime()); // set the Expires header CDateTime now = CDateTime::GetCurrentDateTime(); CDateTime expiryTime = now; if (StringUtils::EqualsNoCase(mimeType, "text/html") || StringUtils::EqualsNoCase(mimeType, "text/css") || StringUtils::EqualsNoCase(mimeType, "application/javascript")) expiryTime += CDateTimeSpan(1, 0, 0, 0); else expiryTime += CDateTimeSpan(365, 0, 0, 0); AddHeader(response, MHD_HTTP_HEADER_EXPIRES, expiryTime.GetAsRFC1123DateTime()); // set the Cache-Control header int maxAge = (expiryTime - now).GetSecondsTotal(); AddHeader(response, MHD_HTTP_HEADER_CACHE_CONTROL, StringUtils::Format("max-age=%d, public", maxAge)); return MHD_YES; }