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) { 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; }