/*MT* MediaTomb - http://www.mediatomb.cc/ file_request_handler.cc - this file is part of MediaTomb. Copyright (C) 2005 Gena Batyan , Sergey 'Jin' Bostandzhyan Copyright (C) 2006-2010 Gena Batyan , Sergey 'Jin' Bostandzhyan , Leonhard Wimmer Copyright (C) 2016-2026 Gerbera Contributors MediaTomb is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. MediaTomb is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License version 2 along with MediaTomb; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. $Id$ */ /// @file request_handler/file_request_handler.cc #define GRB_LOG_FAC GrbLogFacility::requests #include "file_request_handler.h" // API #include "cds/cds_item.h" #include "config/config.h" #include "config/config_val.h" #include "config/result/transcoding.h" #include "config/setup/config_setup_path.h" #include "content/content.h" #include "database/database.h" #include "database/db_param.h" #include "exceptions.h" #include "iohandler/file_io_handler.h" #include "iohandler/seekable_transcode_io_handler.h" #include "metadata/metadata_handler.h" #include "metadata/metadata_service.h" #include "transcoding/transcode_dispatcher.h" #include "upnp/compat.h" #include "upnp/headers.h" #include "upnp/quirks.h" #include "upnp/upnp_common.h" #include "upnp/xml_builder.h" #include "util/grb_net.h" #include "util/tools.h" #include "util/url_utils.h" #include "web/session_manager.h" #include #ifdef HAVE_ZIP #include #include #include #include #endif #include #include FileRequestHandler::FileRequestHandler( const std::shared_ptr& content, const std::shared_ptr& xmlBuilder, const std::shared_ptr& quirks, std::shared_ptr metadataService) : RequestHandler(content, xmlBuilder, quirks) , metadataService(std::move(metadataService)) { } /** * @brief Load a file resource, recovering a stale playlist alias through its source item. * * A playlist reimport can replace a virtual item after its resource URL has * already been sent to a client. If that alias no longer exists, use the URL's * source-object hint only for the original physical file's default content * resource. Keep the client's access checks and reject transformed resources. */ std::shared_ptr FileRequestHandler::loadFileObject(const std::map& params) const { try { return loadObject(params); } catch (const ObjectNotFoundException&) { const auto source = params.find(URL_PARAM_SOURCE_OBJECT_ID); const auto resourceId = params.find(URL_RESOURCE_ID); const auto decimal = [](const std::string& value) { return !value.empty() && std::all_of(value.begin(), value.end(), [](char c) { return c >= '0' && c <= '9'; }); }; if (source == params.end() || resourceId == params.end() || !decimal(source->second) || !decimal(resourceId->second) || params.count(URL_PARAM_TRANSCODE_PROFILE_NAME) || params.count(URL_PARAM_TRANSCODE) || params.count(URL_PARAM_ZIP_REQUEST)) throw; const auto sourceId = stoiString(source->second, -1); const auto resId = stoiString(resourceId->second, -1); if (sourceId <= CDS_ID_ROOT || sourceId == stoiString(getValueOrDefault(params, "object_id")) || resId < 0) throw; auto sourceParams = params; sourceParams["object_id"] = source->second; auto object = loadObject(sourceParams); // Keep the original client group and its access rules. const auto resource = object->getResource(resId); if (object->getEntryType() != CdsEntryType::File || object->isVirtual() || !object->getLocation().is_absolute() || !resource || resource->getHandlerType() != ContentHandler::DEFAULT || resource->getPurpose() != ResourcePurpose::Content || !resource->getAttribute(ResourceAttribute::OFFSET).empty() || !resource->getAttribute(ResourceAttribute::RESOURCE_FILE).empty() || !resource->getParameters().empty() || !resource->getOptions().empty()) throw; return object; } } bool FileRequestHandler::getInfo( const char* filename, UpnpFileInfo* info) { log_debug("Start: {}", filename); auto params = URLUtils::parseParameters(filename, LINK_FILE_REQUEST_HANDLER); auto obj = loadFileObject(params); auto resourceId = parseResourceInfo(params); std::string zipRequest = getValueOrDefault(params, URL_PARAM_ZIP_REQUEST); if (!obj->isItem() && obj->getResourceCount() == 0 && zipRequest.empty()) { throw_std_runtime_error("Requested object {} is not an item or has no resources", filename); } std::string trProfile = getValueOrDefault(params, URL_PARAM_TRANSCODE_PROFILE_NAME); // for transcoded resources res_id will be max std::size_t if (resourceId >= obj->getResourceCount() && trProfile.empty() && zipRequest.empty()) { throw_std_runtime_error("Requested resource {} does not exist", resourceId); } std::string mimeType; auto path = (obj->isItem() || !obj->isVirtual()) ? obj->getLocation() : ""; Headers headers; if (obj->isItem() && !trProfile.empty()) mimeType = getTranscodingInfo(obj, info, path, trProfile, headers); else if (obj->isContainer() && !zipRequest.empty()) mimeType = getZipInfo(obj, info); else mimeType = getResourceInfo(obj, info, resourceId, path, headers); std::string dlnaTransferHeader = xmlBuilder->getDLNATransferHeader(mimeType); if (!dlnaTransferHeader.empty()) { headers.addHeader(UPNP_DLNA_TRANSFER_MODE_HEADER, dlnaTransferHeader); } GrbUpnpFileInfoSetContentType(info, mimeType); if (quirks) quirks->updateHeaders(headers); headers.writeHeaders(info); log_debug("end: Requested {}, ObjectID: {}, Location: {}, MimeType: {}", filename, obj->getID(), path.c_str(), mimeType); return quirks && quirks->getClient(); } std::string FileRequestHandler::getTranscodingInfo( const std::shared_ptr& obj, UpnpFileInfo* info, const std::string& path, const std::string& trProfile, Headers& headers) { getFileInfo(path, info, false, ContentHandler::TRANSCODE, ResourcePurpose::Transcode); auto transcodingProfile = config->getTranscodingProfileListOption(ConfigVal::TRANSCODING_PROFILE_LIST)->getByName(trProfile); if (!transcodingProfile) throw_std_runtime_error("Requested transcoding of file {} but no profile matching the name {} found", path, trProfile); std::string mimeType = transcodingProfile->getTargetMimeType(); auto mimeProperties = transcodingProfile->getTargetMimeProperties(); if (!mimeProperties.empty()) { auto res = obj->getResource(ContentHandler::DEFAULT); std::vector propList = { mimeType }; for (const auto& prop : mimeProperties) { std::string value; if (prop.getAttribute() != ResourceAttribute::MAX) value = res->getAttribute(prop.getAttribute()); if (prop.getMetadata() != MetadataFields::M_MAX) value = obj->getMetaData(prop.getMetadata()); if (!value.empty()) propList.push_back(fmt::format("{}={}", prop.getKey(), value)); } mimeType = fmt::format("{}", fmt::join(propList, ";")); } // A profile with a constant bit rate is served like a file of this length auto mainResource = obj->getResource(ResourcePurpose::Content); auto seekSize = mainResource ? transcodingProfile->getSeekSize(mainResource->getAttribute(ResourceAttribute::DURATION)) : -1; // Generate DLNA Headers, with the values the DIDL has for the transcoded resource auto resource = std::make_shared(ContentHandler::TRANSCODE, ResourcePurpose::Transcode); if (!transcodingProfile->getDlnaProfile().empty()) resource->addOption("dlnaProfile", transcodingProfile->getDlnaProfile()); if (seekSize > 0) resource->addAttribute(ResourceAttribute::SIZE, seekSize); auto mappings = config->getDictionaryOption(ConfigVal::IMPORT_MAPPINGS_MIMETYPE_TO_CONTENTTYPE_LIST); std::string dlnaContentHeader = xmlBuilder->getDLNAContentHeader(getValueOrDefault(mappings, transcodingProfile->getTargetMimeType()), resource, quirks); if (!dlnaContentHeader.empty()) { headers.addHeader(UPNP_DLNA_CONTENT_FEATURES_HEADER, dlnaContentHeader); } if (seekSize > 0) { // libupnp sends Content-Length and answers range requests UpnpFileInfo_set_FileLength(info, seekSize); } else { // The length is unknown. Not UPNP_USING_CHUNKED: libupnp answers 406 Not Acceptable to // an HTTP/1.0 client, as chunked encoding exists only in HTTP/1.1. Without a length the // stream ends when the connection closes, which clients of both versions understand. UpnpFileInfo_set_FileLength(info, UPNP_UNTIL_CLOSE); } return mimeType; } std::string FileRequestHandler::getZipInfo( const std::shared_ptr& obj, UpnpFileInfo* info) { std::string mimeType = "application/zip"; #ifdef HAVE_ZIP UpnpFileInfo_set_IsReadable(info, true); UpnpFileInfo_set_FileLength(info, UPNP_USING_CHUNKED); #else UpnpFileInfo_set_IsReadable(info, false); #endif UpnpFileInfo_set_LastModified(info, obj->getMTime().count()); UpnpFileInfo_set_IsDirectory(info, false); return mimeType; } void FileRequestHandler::getFileInfo( const std::string& path, UpnpFileInfo* info, bool isResourceFile, ContentHandler handlerType, ResourcePurpose purpose) { if (!path.empty()) { // Check the path (if its real) is accessible struct stat statbuf { }; int ret = stat(path.c_str(), &statbuf); if (ret != 0) { if (isResourceFile) { throw ResourceNotFoundException(fmt::format("{} file {} is not available.", EnumMapper::getPurposeDisplay(purpose), path)); } throw_fmt_system_error("Failed to open {}", path); } // If we get to here we can read the thing UpnpFileInfo_set_IsReadable(info, true); UpnpFileInfo_set_LastModified(info, statbuf.st_mtime); bool isDir = (handlerType == ContentHandler::DEFAULT && S_ISDIR(statbuf.st_mode)); UpnpFileInfo_set_IsDirectory(info, isDir); if (!isDir) UpnpFileInfo_set_FileLength(info, statbuf.st_size); } } std::string FileRequestHandler::getResourceInfo( std::shared_ptr& obj, UpnpFileInfo* info, std::size_t resourceId, std::string path, Headers& headers) { UpnpFileInfo_set_IsReadable(info, false); UpnpFileInfo_set_LastModified(info, obj->getMTime().count()); UpnpFileInfo_set_IsDirectory(info, true); auto resource = obj->getResource(resourceId); // Check if the resource is actually another external file, and if it exists bool isResourceFile = false; auto resPath = resource->getAttribute(ResourceAttribute::RESOURCE_FILE); isResourceFile = !resPath.empty() && resPath != obj->getLocation(); if (isResourceFile) { log_debug("Resource is file: {}", path); path = resPath; } getFileInfo(path, info, isResourceFile, resource->getHandlerType(), resource->getPurpose()); auto item = std::dynamic_pointer_cast(obj); std::string mimeType = item ? item->getMimeType() : ""; if (resource->getHandlerType() != ContentHandler::DEFAULT) { auto metadataHandler = getResourceMetadataHandler(obj, resource); std::string protocolInfo = resource->getAttribute(ResourceAttribute::PROTOCOLINFO); if (!protocolInfo.empty()) { mimeType = getMTFromProtocolInfo(protocolInfo); } if (mimeType.empty()) { mimeType = metadataHandler->getMimeType(); } if (startswith(mimeType, "text")) mimeType = fmt::format("{}; charset=utf-8", mimeType); log_debug("getInfo {}:{}", obj->getID(), resource->getResId()); auto resSize = resource->getAttribute(ResourceAttribute::SIZE); if (!resSize.empty()) { UpnpFileInfo_set_FileLength(info, stoiString(resSize)); } else { auto ioHandler = metadataHandler->serveContent(obj, resource); if (ioHandler) { // Get size ioHandler->open(UPNP_READ); ioHandler->seek(0L, SEEK_END); off_t size = ioHandler->tell(); ioHandler->close(); log_debug("info {}={}", obj->getID(), size); UpnpFileInfo_set_FileLength(info, size); } else { UpnpFileInfo_set_FileLength(info, 0); } } } else if (item) { if (quirks) quirks->addCaptionInfo(item, headers); // Generate DNLA Headers auto mappings = config->getDictionaryOption(ConfigVal::IMPORT_MAPPINGS_MIMETYPE_TO_CONTENTTYPE_LIST); std::string dlnaContentHeader = xmlBuilder->getDLNAContentHeader(getValueOrDefault(mappings, mimeType), resource, quirks); if (!dlnaContentHeader.empty()) { headers.addHeader(UPNP_DLNA_CONTENT_FEATURES_HEADER, dlnaContentHeader); } } if (mimeType.empty() && item) mimeType = item->getMimeType(); return mimeType; } std::shared_ptr FileRequestHandler::getResourceMetadataHandler( std::shared_ptr& obj, std::shared_ptr& resource) const { log_debug("start: {}", obj->getID()); auto resHandler = resource->getHandlerType(); if (resource->getAttribute(ResourceAttribute::RESOURCE_FILE).empty()) { auto objID = stoiString(resource->getAttribute(ResourceAttribute::FANART_OBJ_ID)); auto resID = stoiString(resource->getAttribute(ResourceAttribute::FANART_RES_ID)); try { auto resObj = (objID > CDS_ID_ROOT && objID != obj->getID()) ? database->loadObject(objID) : nullptr; if (resObj) { auto resRes = resObj->getResource(resID); if (resRes) { log_debug("Switch obj {}:{} to {}:{}", obj->getID(), resource->getResId(), objID, resID); obj = resObj; resource = resRes; return getResourceMetadataHandler(obj, resource); } } } catch (const std::runtime_error& ex) { log_error("{}", EX(ex)); } } log_debug("end: {}", obj->getID()); return metadataService->getHandler(resHandler); } #ifdef HAVE_ZIP class ContainerProgressListener : public libzippp::ZipProgressListener { private: bool done { false }; std::condition_variable cond; public: ContainerProgressListener() { } virtual ~ContainerProgressListener() { } void wait(std::unique_lock& lock) { if (!done) cond.wait(lock); // waiting for the task to complete } int cancel() override { return 0; } void progression(double p) override { log_debug("-- Progress {}: ", p); if (p == 1) { log_debug("-- Done {}: ", p); done = true; cond.notify_one(); } } }; static int archive( const fs::path& path, const std::map& content) { log_debug("start: {}", path.string()); libzippp::ZipArchive zipFile(path); zipFile.open(libzippp::ZipArchive::Write); // register the listener ContainerProgressListener cpl; std::mutex mutex; std::unique_lock lock(mutex); zipFile.addProgressListener(&cpl); if (!content.empty()) zipFile.setProgressPrecision(1.0 / content.size()); for (auto&& [name, path] : content) { log_debug("Add {} : {}", name, path.c_str()); zipFile.addFile(fmt::format("{}{}", name, path.extension().c_str()).c_str(), path.c_str()); } zipFile.close(); log_debug("wait: {}", path.string()); cpl.wait(lock); log_debug("end: {}", path.string()); return 0; } #endif std::unique_ptr FileRequestHandler::openResource( std::shared_ptr& obj, std::size_t resourceId) { auto resource = obj->getResource(resourceId); auto metadataHandler = getResourceMetadataHandler(obj, resource); log_debug("serveContent {}:{}", obj->getID(), resource->getResId()); return metadataHandler->serveContent(obj, resource); } std::unique_ptr FileRequestHandler::openTranscoding( const std::shared_ptr& obj, const std::string& path, const std::string& trProfile, const std::string& group, const std::map& params) { auto transcodingProfile = config->getTranscodingProfileListOption(ConfigVal::TRANSCODING_PROFILE_LIST)->getByName(trProfile); if (!transcodingProfile) throw_std_runtime_error("Requested transcoding of file {} but no profile matching the name {} found", path.c_str(), trProfile); std::string range = getValueOrDefault(params, "range"); auto mainResource = obj->getResource(ResourcePurpose::Content); if (range.empty() && mainResource && transcodingProfile->getSeekSize(mainResource->getAttribute(ResourceAttribute::DURATION)) > 0) return std::make_unique(content, transcodingProfile, path, obj, group); auto transcodeDispatcher = std::make_unique(content); return transcodeDispatcher->serveContent(transcodingProfile, path, obj, group, range); } std::unique_ptr FileRequestHandler::openZip( const std::shared_ptr& obj) { #ifdef HAVE_ZIP auto path = ConfigPathSetup::Home / fmt::format("Download-{}.zip", obj->getID()); auto browseParam = BrowseParam(obj, BROWSE_DIRECT_CHILDREN | BROWSE_ITEMS); auto result = database->browse(browseParam); std::map folderContent; log_debug("zipRequest: {}", result.size()); for (auto&& cdsObj : result) { folderContent[cdsObj->getSortKey()] = cdsObj->getLocation(); } try { archive(path, folderContent); return std::make_unique(path); } catch (std::runtime_error& ex) { log_debug("{}", EX(ex)); return nullptr; } #else return nullptr; #endif } std::unique_ptr FileRequestHandler::open( const char* filename, const std::shared_ptr& quirks, enum UpnpOpenFileMode mode) { log_debug("start: {}", filename); // We explicitly do not support UPNP_WRITE due to security reasons. if (mode != UPNP_READ) { throw_std_runtime_error("UPNP_WRITE unsupported"); } auto params = URLUtils::parseParameters(filename, LINK_FILE_REQUEST_HANDLER); auto obj = loadFileObject(params); auto resourceId = parseResourceInfo(params); auto res = obj->getResource(resourceId); // Transcoding std::string trProfile = getValueOrDefault(params, URL_PARAM_TRANSCODE_PROFILE_NAME); std::string zipRequest = getValueOrDefault(params, URL_PARAM_ZIP_REQUEST); // Serve metadata resources if (trProfile.empty() && zipRequest.empty() && res && res->getHandlerType() != ContentHandler::DEFAULT) { return openResource(obj, resourceId); } auto path = (obj->isItem() || !obj->isVirtual()) ? obj->getLocation() : ""; if (path.empty() && zipRequest.empty()) { throw_std_runtime_error("Object location for {} not set", filename); } auto it = params.find(CLIENT_GROUP_TAG); std::string group = DEFAULT_CLIENT_GROUP; if (it != params.end()) { group = it->second; } if (!trProfile.empty()) { return openTranscoding(obj, path, trProfile, group, params); } if (!zipRequest.empty()) { return openZip(obj); } content->triggerPlayHook(group, obj); off_t offset = 0; if (res) { offset = stoulString(res->getAttribute(ResourceAttribute::OFFSET)); double sampleRate = double(stoulString(res->getAttribute(ResourceAttribute::SAMPLEFREQUENCY))) / 1000.0; offset = offset * sampleRate / OFFSET_FACTOR + offset % OFFSET_FACTOR; } // Boring old file return std::make_unique(path, offset); } std::size_t FileRequestHandler::parseResourceInfo( const std::map& params) { std::size_t resourceId = 0; try { auto resIdParam = params.at(URL_RESOURCE_ID); if (resIdParam != URL_VALUE_TRANSCODE_NO_RES_ID) { resourceId = stoiString(resIdParam); } else { resourceId = std::numeric_limits::max(); } } catch (const std::out_of_range&) { } log_debug("Resource ID: {}", resourceId); return resourceId; }