src/server.cc
Rama master · réplica de lectura del repositorio original.
/*MT*
MediaTomb - http://www.mediatomb.cc/
server.cc - this file is part of MediaTomb.
Copyright (C) 2005 Gena Batyan <bgeradz@mediatomb.cc>,
Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>
Copyright (C) 2006-2010 Gena Batyan <bgeradz@mediatomb.cc>,
Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>,
Leonhard Wimmer <leo@mediatomb.cc>
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 server.cc
#define GRB_LOG_FAC GrbLogFacility::server
#include "server.h" // API
#include "action_request.h"
#include "config/config.h"
#include "config/config_definition.h"
#include "config/config_setup.h"
#include "config/config_val.h"
#include "content/content_manager.h"
#include "context.h"
#include "database/database.h"
#include "exceptions.h"
#include "iohandler/io_handler.h"
#include "metadata/metadata_service.h"
#include "request_handler/device_description_handler.h"
#include "request_handler/file_request_handler.h"
#include "request_handler/request_handler.h"
#include "request_handler/ui_handler.h"
#include "request_handler/upnp_desc_handler.h"
#include "subscription_request.h"
#include "upnp/client_manager.h"
#include "upnp/clients.h"
#include "upnp/compat.h"
#include "upnp/conn_mgr_service.h"
#include "upnp/cont_dir_service.h"
#include "upnp/headers.h"
#include "upnp/mr_reg_service.h"
#include "upnp/upnp_common.h"
#include "upnp/xml_builder.h"
#include "util/grb_net.h"
#include "util/mime.h"
#include "util/string_converter.h"
#include "util/tools.h"
#include "util/url_utils.h"
#include "web/session_manager.h"
#include "web/web_request_handler.h"
#ifdef HAVE_CURL
#include "request_handler/url_request_handler.h"
#endif
#ifdef HAVE_INOTIFY
#include "content/inotify/autoscan_inotify.h"
#include "content/inotify/scripting_inotify.h"
#endif
#ifdef ONLINE_SERVICES
#include "content/onlineservice/online_service.h"
#endif
#ifdef HAVE_CURL
#include <curl/curl.h>
#endif
#include <upnpconfig.h>
#ifdef UPNP_HAVE_TOOLS
#include <upnptools.h>
#endif
#include <regex>
#include <thread>
constexpr auto DEVICE_DESCRIPTION_PATH = std::string_view(UPNP_DESC_SCPD_URL UPNP_DESC_DEVICE_DESCRIPTION);
Server::Server(std::shared_ptr<Config> config)
: config(std::move(config))
{
}
void Server::init(
const std::shared_ptr<ConfigDefinition>& definition,
bool offln,
bool dropDatabase,
bool initLastFM)
{
offline = offln;
#ifdef HAVE_CURL
curl_global_init(CURL_GLOBAL_ALL);
#endif
// initialize what is needed
self = shared_from_this();
timer = std::make_shared<Timer>();
timer->run();
mime = std::make_shared<Mime>(config);
converterManager = std::make_shared<ConverterManager>(config);
database = Database::createInstance(config, mime, converterManager, timer);
// special start mode
if (dropDatabase) {
database->dropTables();
return;
}
database->init();
config->updateConfigFromDatabase(database);
serverUDN = config->getOption(ConfigVal::SERVER_UDN);
if (serverUDN == GRB_UDN_AUTO) {
serverUDN = config->generateUDN(database);
}
aliveAdvertisementInterval = config->getIntOption(ConfigVal::SERVER_ALIVE_INTERVAL);
clientManager = std::make_shared<ClientManager>(config, database, self);
sessionManager = std::make_shared<Web::SessionManager>(config, timer);
context = std::make_shared<Context>(definition, config, clientManager, mime, database, sessionManager, converterManager);
content = std::make_shared<ContentManager>(context, self, timer);
#ifdef HAVE_LASTFM
#ifndef HAVE_LASTFMLIB
// special start mode
if (initLastFM) {
content->initLastFM();
return;
}
#endif
#endif
metadataService = std::make_shared<MetadataService>(context, content);
}
struct UpnpDesc {
enum Upnp_DescType_e mode;
std::string desc;
int len;
};
void Server::run()
{
log_debug("Starting...");
// run what is needed
try {
content->run();
} catch (const std::runtime_error& ex) {
log_error("Activating ContentService failed {}", EX(ex));
throw UpnpException(500, fmt::format("run: Activating ContentService failed {}", EX(ex)));
}
std::string iface = config->getOption(ConfigVal::SERVER_NETWORK_INTERFACE);
ip = config->getOption(ConfigVal::SERVER_IP);
if (!ip.empty() && !iface.empty())
throw_std_runtime_error("You cannot specify interface {} and IP {} at the same time", iface, ip);
if (iface.empty())
iface = GrbNet::ipToInterface(ip);
if (!ip.empty() && iface.empty())
throw_std_runtime_error("Could not find IP: {}", ip);
// check webroot directory
std::string webRoot = config->getOption(ConfigVal::SERVER_WEBROOT);
if (webRoot.empty()) {
throw_std_runtime_error("Invalid web server root directory {}", webRoot);
}
auto configPort = static_cast<in_port_t>(config->getUIntOption(ConfigVal::SERVER_PORT));
auto ret = startupInterface(iface, configPort);
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, fmt::format("UpnpInit failed {} {}", iface, port));
}
if (configPort > 0 && port != configPort) {
throw UpnpException(ret, fmt::format("LibUPnP failed to bind to request port! Bound to {}, requested: {}", port, configPort));
}
log_debug("webroot: {}", webRoot);
log_debug("Adding root virtual dir");
ret = UpnpAddVirtualDir("/", this, nullptr);
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, "run: UpnpAddVirtualDir failed!");
}
ret = registerVirtualDirCallbacks();
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, "run: UpnpSetVirtualDirCallbacks failed");
}
log_debug("Creating UpnpXMLBuilders");
upnpXmlBuilder = std::make_shared<UpnpXMLBuilder>(context, getVirtualUrl());
webXmlBuilder = std::make_shared<UpnpXMLBuilder>(context, getExternalUrl());
auto net = std::make_shared<GrbNet>(getIp(), AF_INET);
auto userAgent = fmt::format("Gerbera-UPnP-Server/{} Portable SDK for UPnP devices/{}", GERBERA_VERSION, UPNP_VERSION_STRING);
clientManager->refresh();
clientManager->removeClient(getIp(), userAgent); // local calls will always be treated as calls from gerbera
auto quirks = std::make_shared<Quirks>(upnpXmlBuilder, context->getClients(), net, std::move(userAgent), nullptr);
auto devDescHdl = std::make_shared<DeviceDescriptionHandler>(content, webXmlBuilder, quirks, getIp(), getPort());
int activeUpnpDescription = config->getBoolOption(ConfigVal::UPNP_DYNAMIC_DESCRIPTION) ? 0 : 1;
// register root device with the library
auto upnpDesc = std::vector<UpnpDesc> {
{ UPNPREG_URL_DESC, fmt::format("http://{}{}", GrbNet::renderWebUri(getIp(), getPort()), DEVICE_DESCRIPTION_PATH), -1 },
{ UPNPREG_BUF_DESC, devDescHdl->renderDeviceDescription(getIp(), getPort(), nullptr), 0 }
};
log_debug("Registering with UPnP... ({})", upnpDesc[activeUpnpDescription].desc);
ret = UpnpRegisterRootDevice2(
upnpDesc[activeUpnpDescription].mode,
upnpDesc[activeUpnpDescription].desc.c_str(),
upnpDesc[activeUpnpDescription].len >= 0 ? upnpDesc[activeUpnpDescription].desc.length() + 1 : upnpDesc[activeUpnpDescription].len,
false,
[](auto eventType, auto event, auto cookie) { return static_cast<Server*>(cookie)->handleUpnpRootDeviceEvent(eventType, event); },
this,
&rootDeviceHandle);
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, "run: UpnpRegisterRootDevice2 failed");
}
log_debug("Registering UPnP client...");
ret = UpnpRegisterClient(
[](auto eventType, auto event, auto cookie) { return static_cast<Server*>(cookie)->handleUpnpClientEvent(eventType, event); },
this,
&clientHandle);
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, "run: UpnpRegisterClient failed");
}
log_debug("Creating ContentDirectoryService");
serviceList.push_back(std::make_unique<ContentDirectoryService>(context, upnpXmlBuilder, rootDeviceHandle,
config->getIntOption(ConfigVal::SERVER_UPNP_TITLE_AND_DESC_STRING_LIMIT), offline));
log_debug("Creating ConnectionManagerService");
serviceList.push_back(std::make_unique<ConnectionManagerService>(context, upnpXmlBuilder, rootDeviceHandle));
log_debug("Creating MRRegistrarService");
serviceList.push_back(std::make_unique<MRRegistrarService>(context, upnpXmlBuilder, rootDeviceHandle));
// The advertisement will be sent by LibUPnP every (A/2)-30 seconds, and will have a cache-control max-age of A where A is
// the value configured here. Ex: A value of 62 will result in an SSDP advertisement being sent every second.
log_debug("Will send UPnP Alive advertisements every {} seconds", (aliveAdvertisementInterval / 2) - 30);
ret = UpnpSendAdvertisement(rootDeviceHandle, aliveAdvertisementInterval);
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, fmt::format("run: UpnpSendAdvertisement {} failed", aliveAdvertisementInterval));
}
if (config->getBoolOption(ConfigVal::UPNP_LITERAL_HOST_REDIRECTION)) {
UpnpSetAllowLiteralHostRedirection(1);
}
{
std::string url = getVirtualUrl();
writeBookmark(url);
log_info("The Web UI can be reached by following this link: {}", url);
validHosts = std::vector<std::string> {
std::string(UpnpGetServerIpAddress()),
};
if (!url.empty()) {
validHosts.push_back(url);
}
url = getExternalUrl();
if (!url.empty()) {
validHosts.push_back(url);
}
if (!ip6.empty()) {
validHosts.push_back(ip6);
}
if (!ulaGuaIp6.empty()) {
validHosts.push_back(ulaGuaIp6);
}
}
{
corsHosts = {
"'self'",
fmt::format("http://{}", UpnpGetServerIpAddress()),
};
std::string url = getVirtualUrl();
std::regex re(":[0-9]+");
if (!url.empty()) {
corsHosts.push_back(url);
corsHosts.push_back(std::regex_replace(url, re, ""));
}
url = getExternalUrl();
if (!url.empty()) {
corsHosts.push_back(url);
corsHosts.push_back(std::regex_replace(url, re, ""));
}
if (!ip6.empty()) {
corsHosts.push_back(fmt::format("http://[{}]", ip6));
}
if (!ulaGuaIp6.empty()) {
corsHosts.push_back(fmt::format("http://[{}]", ulaGuaIp6));
}
// CORS does not support multiple values
UpnpSetWebServerCorsString("*");
}
UpnpSetHostValidateCallback(HostValidateCallback, this);
running = true;
}
std::string Server::getIp() const
{
if (port > 0 && !ip.empty())
return ip;
if (port6 > 0 && !ip6.empty())
return ip6;
if (ulaGuaPort6 > 0 && !ulaGuaIp6.empty())
return ulaGuaIp6;
return "127.0.0.1";
}
in_port_t Server::getPort() const
{
if (port > 0 && !ip.empty())
return port;
if (port6 > 0 && !ip6.empty())
return port6;
if (ulaGuaPort6 > 0 && !ulaGuaIp6.empty())
return ulaGuaPort6;
return -1;
}
int Server::startupInterface(const std::string& iface, in_port_t inPort)
{
log_info("Initialising UPnP with interface: {}, port: {}",
iface.empty() ? "<unset>" : iface, inPort == 0 ? "<unset>" : fmt::to_string(inPort));
const char* ifName = iface.empty() ? nullptr : iface.c_str();
int ret = UPNP_E_INIT_FAILED;
#ifdef UPNP_HAVE_TOOLS
UpnpSetMaxJobsTotal(config->getUIntOption(ConfigVal::SERVER_UPNP_MAXJOBS));
#endif
for (std::size_t attempt = 0; attempt < 4; attempt++) {
ret = UpnpInit2(ifName, inPort);
if (ret == UPNP_E_SUCCESS)
break;
#ifdef UPNP_HAVE_TOOLS
auto errorMsg = UpnpGetErrorMessage(ret);
#else
auto errorMsg = "";
#endif
log_warning("UPnP Init {}:{} failed: {} ({}). Retrying in {} seconds...",
ifName ? ifName : "<unset>", inPort,
errorMsg ? errorMsg : "???", ret, attempt + 1);
std::this_thread::sleep_for(std::chrono::seconds(attempt + 1));
}
if (ret != UPNP_E_SUCCESS) {
throw UpnpException(ret, fmt::format("run: UpnpInit failed with {} {}", ifName ? ifName : "<unset>", inPort));
}
port = UpnpGetServerPort();
/* The IP libupnp picks is not always the same as passed into config, as we map it to an interface */
ip = UpnpGetServerIpAddress();
log_info("IPv4: Server bound to: {}:{}", ip, port);
port6 = UpnpGetServerPort6();
ip6 = UpnpGetServerIp6Address();
if (port6 > 0)
log_info("IPv6: Server bound to: {}:{}", ip6, port6);
ulaGuaPort6 = UpnpGetServerUlaGuaPort6();
ulaGuaIp6 = UpnpGetServerUlaGuaIp6Address();
if (ulaGuaPort6 > 0)
log_info("IPv6 ULA/GLA: Server bound to: {}:{}", ulaGuaIp6, ulaGuaPort6);
return ret;
}
void Server::writeBookmark(const std::string& addr)
{
const std::string data = config->getBoolOption(ConfigVal::SERVER_UI_ENABLED)
? httpRedirectTo(addr)
: httpRedirectTo(addr, "disabled.html");
fs::path path = config->getOption(ConfigVal::SERVER_BOOKMARK_FILE);
log_debug("Writing bookmark file to: {}", path.c_str());
GrbFile(path).writeTextFile(data);
}
void Server::emptyBookmark()
{
const std::string_view data = "<html><body><h1>Gerbera Media Server is not running.</h1><p>Please start it and try again.</p></body></html>";
fs::path path = config->getOption(ConfigVal::SERVER_BOOKMARK_FILE);
log_debug("Clearing bookmark file at: {}", path.c_str());
GrbFile(path).writeTextFile(data);
}
std::string Server::getVirtualUrl() const
{
auto virtUrl = config->getOption(ConfigVal::VIRTUAL_URL);
if (virtUrl.empty()) {
virtUrl = GrbNet::renderWebUri(getIp(), getPort());
}
if (!startswith(virtUrl, "http")) { // url does not start with http
virtUrl = fmt::format("http://{}", virtUrl);
}
if (virtUrl.back() == '/')
virtUrl.pop_back();
return virtUrl;
}
std::string Server::getExternalUrl() const
{
auto virtUrl = config->getOption(ConfigVal::EXTERNAL_URL);
if (virtUrl.empty()) {
virtUrl = config->getOption(ConfigVal::VIRTUAL_URL);
if (virtUrl.empty()) {
virtUrl = GrbNet::renderWebUri(getIp(), getPort());
}
}
if (!startswith(virtUrl, "http")) { // url does not start with http
virtUrl = fmt::format("http://{}", virtUrl);
}
if (virtUrl.back() == '/')
virtUrl.pop_back();
return virtUrl;
}
bool Server::getShutdownStatus() const
{
return server_shutdown_flag;
}
void Server::shutdown()
{
int ret = 0; // return code
emptyBookmark();
server_shutdown_flag = true;
log_debug("Server shutting down");
ret = UpnpUnRegisterClient(clientHandle);
if (ret != UPNP_E_SUCCESS) {
log_error("UpnpUnRegisterClient failed ({})", ret);
}
ret = UpnpUnRegisterRootDevice(rootDeviceHandle);
if (ret != UPNP_E_SUCCESS) {
log_error("UpnpUnRegisterRootDevice failed ({})", ret);
}
UpnpSetHostValidateCallback(nullptr, nullptr);
#ifdef HAVE_CURL
curl_global_cleanup();
#endif
log_debug("now calling upnp finish");
ret = UpnpFinish();
if (ret != UPNP_E_SUCCESS) {
log_error("UpnpFinish failed ({})", ret);
}
if (content) {
content->shutdown();
content.reset();
}
sessionManager.reset();
if (database->threadCleanupRequired()) {
try {
database->threadCleanup();
} catch (const std::runtime_error& ex) {
}
}
database->shutdown();
database.reset();
timer->shutdown();
timer.reset();
context.reset();
mime.reset();
clientManager.reset();
metadataService.reset();
upnpXmlBuilder.reset();
webXmlBuilder.reset();
for (auto&& svc : serviceList)
svc.reset();
serviceList.clear();
log_debug("Server down");
}
int Server::handleUpnpRootDeviceEvent(Upnp_EventType eventType, const void* event)
{
int ret = UPNP_E_SUCCESS; // general purpose return code
log_debug("start");
// check parameters
if (!event) {
log_debug("handleUpnpRootDeviceEvent: NULL event structure");
return UPNP_E_BAD_REQUEST;
}
// dispatch event based on event type
switch (eventType) {
case UPNP_CONTROL_ACTION_REQUEST:
log_debug("UPNP_CONTROL_ACTION_REQUEST");
try {
auto request = ActionRequest(upnpXmlBuilder, clientManager, const_cast<UpnpActionRequest*>(static_cast<const UpnpActionRequest*>(event)));
routeActionRequest(request);
request.update();
} catch (const UpnpException& upnpE) {
ret = upnpE.getErrorCode();
log_warning("UpnpException: {} -> {}", ret, EX(upnpE));
UpnpActionRequest_set_ErrCode(const_cast<UpnpActionRequest*>(static_cast<const UpnpActionRequest*>(event)), ret);
UpnpActionRequest_strcpy_ErrStr(const_cast<UpnpActionRequest*>(static_cast<const UpnpActionRequest*>(event)), upnpE.what());
} catch (const std::runtime_error& e) {
log_warning("Exception: {}", EX(e));
ret = UPNP_E_BAD_REQUEST;
}
break;
case UPNP_EVENT_SUBSCRIPTION_REQUEST:
log_debug("UPNP_EVENT_SUBSCRIPTION_REQUEST");
try {
auto request = SubscriptionRequest(const_cast<UpnpSubscriptionRequest*>(static_cast<const UpnpSubscriptionRequest*>(event)));
routeSubscriptionRequest(request);
} catch (const UpnpException& upnpE) {
log_warning("Subscription exception: {}", EX(upnpE));
ret = upnpE.getErrorCode();
}
break;
default:
// unhandled event type
log_warning("unsupported event type: {}", eventType);
ret = UPNP_E_BAD_REQUEST;
break;
}
log_debug("returning {}", ret);
return ret;
}
int Server::handleUpnpClientEvent(Upnp_EventType eventType, const void* event)
{
// check parameters
if (!event) {
log_debug("handleUpnpClientEvent: NULL event structure");
return UPNP_E_BAD_REQUEST;
}
switch (eventType) {
case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
case UPNP_DISCOVERY_SEARCH_RESULT: {
auto dEvent = static_cast<const UpnpDiscovery*>(event);
const char* userAgent = UpnpDiscovery_get_Os_cstr(dEvent);
auto destAddr = std::make_shared<GrbNet>(UpnpDiscovery_get_DestAddr(dEvent));
const char* location = UpnpDiscovery_get_Location_cstr(dEvent);
clientManager->addClientByDiscovery(destAddr, userAgent, location);
break;
}
default:
break;
}
return 0;
}
void Server::routeActionRequest(ActionRequest& request)
{
log_debug("start");
// make sure the request is for our device
if (request.getUDN() != serverUDN) {
// not for us
throw UpnpException(UPNP_E_BAD_REQUEST, "routeActionRequest: request not for this device");
}
// we need to match the serviceID to one of our services
for (auto&& svc : serviceList) {
if (svc->isActiveMatch(request.getServiceID())) {
log_debug("action request for {}", request.getServiceID());
svc->processActionRequest(request);
return;
}
}
// cp is asking for a nonexistent service, or for a service
// that does not support any actions
log_debug("Service {} does not exist or action {} not supported", request.getServiceID(), request.getActionName());
throw UpnpException(UPNP_E_BAD_REQUEST, "Service does not exist or action not supported");
}
void Server::routeSubscriptionRequest(const SubscriptionRequest& request) const
{
// make sure that the request is for our device
if (request.getUDN() != serverUDN) {
// not for us
log_debug("request not for this device: {} vs {}", request.getUDN(), serverUDN);
throw UpnpException(UPNP_E_BAD_REQUEST, "routeSubscriptionRequest: request not for this device");
}
// we need to match the serviceID to one of our services
for (auto&& svc : serviceList) {
if (svc->isActiveMatch(request.getServiceID())) {
log_debug("subscription request for {}", request.getServiceID());
if (svc->processSubscriptionRequest(request))
return;
}
}
// cp asks for a nonexistent service or for a service that
// does not support subscriptions
log_debug("Service {} does not exist or subscriptions not supported", request.getServiceID());
throw UpnpException(UPNP_E_BAD_REQUEST, "Service does not exist or subscriptions not supported");
}
/// @brief sendSubscriptionUpdate
void Server::sendSubscriptionUpdate(const std::string& updateString, const std::string& serviceId)
{
for (auto&& svc : serviceList) {
if (svc->isActiveMatch(serviceId)) {
log_debug("subscription update for {}", serviceId);
if (svc->sendSubscriptionUpdate(updateString))
return;
}
}
}
std::unique_ptr<RequestHandler> Server::createRequestHandler(
const char* filename,
const std::shared_ptr<Quirks>& quirks) const
{
std::string link = URLUtils::urlUnescape(filename);
log_debug("Filename: {}", filename);
if (startswith(link, fmt::format("/{}", CONTENT_MEDIA_HANDLER))) {
return std::make_unique<FileRequestHandler>(content, upnpXmlBuilder, quirks, metadataService);
}
if (startswith(link, fmt::format("/{}", CONTENT_UI_HANDLER))) {
auto&& parameters = URLUtils::getQuery(filename);
auto params = URLUtils::dictDecode(parameters);
auto it = params.find(URL_REQUEST_TYPE);
std::string rType = it != params.end() && !it->second.empty() ? it->second : "index";
return Web::createWebRequestHandler(context, content, self, webXmlBuilder, quirks, rType);
}
if (startswith(link, DEVICE_DESCRIPTION_PATH) || endswith(link, UPNP_DESC_DEVICE_DESCRIPTION)) {
return std::make_unique<DeviceDescriptionHandler>(content, upnpXmlBuilder, quirks, getIp(), getPort());
}
if (startswith(link, UPNP_DESC_SCPD_URL) || endswith(link, UPNP_DESC_CDS_SCPD_URL) || endswith(link, UPNP_DESC_CM_SCPD_URL) || endswith(link, UPNP_DESC_MRREG_SCPD_URL)) {
return std::make_unique<UpnpDescHandler>(content, upnpXmlBuilder, quirks);
}
if (link == "/" || startswith(link, "/index.html")
|| startswith(link, "/favicon.ico")
|| startswith(link, "/assets")
|| startswith(link, "/vendor")
|| startswith(link, "/doc")
|| startswith(link, "/dev")
|| startswith(link, "/js")
|| startswith(link, "/css")
|| startswith(link, "/icons")) {
return std::make_unique<UIHandler>(content, upnpXmlBuilder, quirks, self);
}
#ifdef HAVE_CURL
if (startswith(link, fmt::format("/{}", CONTENT_ONLINE_HANDLER))) {
return std::make_unique<URLRequestHandler>(content, upnpXmlBuilder, quirks);
}
#endif
throw_std_runtime_error("No valid handler type in {}", filename);
}
std::shared_ptr<Quirks> Server::getQuirks(const UpnpFileInfo* info, bool isWeb) const
{
auto ctrlPtIPAddr = std::make_shared<GrbNet>(UpnpFileInfo_get_CtrlPtIPAddr(info));
auto headers = std::make_shared<Headers>(info);
// HINT: most clients do not report exactly the same User-Agent for UPnP services and file request.
std::string userAgent = UpnpFileInfo_get_Os_cstr(info);
return std::make_shared<Quirks>(isWeb ? webXmlBuilder : upnpXmlBuilder, context->getClients(), ctrlPtIPAddr, std::move(userAgent), headers);
}
int Server::HostValidateCallback(const char* host, void* cookie)
{
auto hostStr = std::string(host);
for (auto&& name : static_cast<Server*>(cookie)->validHosts) {
if (hostStr.find(name) != std::string::npos) {
return UPNP_E_SUCCESS;
}
if (name.find(hostStr) != std::string::npos) {
return UPNP_E_SUCCESS;
}
}
auto config = static_cast<Server*>(cookie)->config;
log_warning("Rejected attempt to load host '{}' as it does not match configured virtualURL/externalURL: '{}'/'{}'. "
"See {}/config-server.html#virtual-url",
host, config->getOption(ConfigVal::VIRTUAL_URL),
config->getOption(ConfigVal::EXTERNAL_URL),
config->getOption(ConfigVal::SERVER_UI_DOCUMENTATION_USER));
return UPNP_E_BAD_HTTPMSG;
}
int Server::GetInfoCallback(
const char* filename,
UpnpFileInfo* info,
const void* cookie,
const void** requestCookie)
{
try {
log_debug("getInfo({})", filename);
auto server = static_cast<const Server*>(cookie);
auto quirks = server->getQuirks(info, startswith(filename, fmt::format("/{}", CONTENT_UI_HANDLER)));
auto client = quirks->getClient();
if (quirks && !quirks->isAllowed()) {
auto clientIp = client && client->addr ? client->addr->getNameInfo(false) : "unknown";
log_debug("Client blocked {}", clientIp);
return -1;
}
auto reqHandler = server->createRequestHandler(filename, quirks);
std::string link = URLUtils::urlUnescape(filename);
reqHandler->getInfo(startswith(link, fmt::format("/{}", CONTENT_UI_HANDLER)) ? filename : link.c_str(), info);
*requestCookie = client;
return 0;
} catch (const ServerShutdownException&) {
return -1;
} catch (const ResourceNotFoundException& stex) {
log_warning("ResourceNotFoundException: {}", EX(stex));
return -1;
} catch (const std::runtime_error& e) {
log_error("Exception: {}", EX(e));
return -1;
} catch (const std::logic_error& ex) {
log_error("Exception: logic error {}", EX(ex));
return -1;
}
}
UpnpWebFileHandle Server::OpenCallback(
const char* filename,
enum UpnpOpenFileMode mode,
const void* cookie,
const void* requestCookie)
{
try {
log_debug("open({})", filename);
auto server = static_cast<const Server*>(cookie);
if (server->getShutdownStatus())
return nullptr;
auto client = requestCookie ? static_cast<const ClientObservation*>(requestCookie) : nullptr;
auto quirks = client ? std::make_shared<Quirks>(client) : nullptr;
if (quirks && !quirks->isAllowed()) {
auto clientIp = client && client->addr ? client->addr->getNameInfo(false) : "unknown";
log_debug("Client blocked {}", clientIp);
return nullptr;
}
auto reqHandler = server->createRequestHandler(filename, quirks);
std::string link = URLUtils::urlUnescape(filename);
auto ioHandler = reqHandler->open(startswith(link, fmt::format("/{}", CONTENT_UI_HANDLER)) ? filename : link.c_str(), quirks, mode);
if (ioHandler) {
ioHandler->open(mode);
return ioHandler.release();
}
log_warning("No Handler for {}", link);
return nullptr;
} catch (const ServerShutdownException&) {
return nullptr;
} catch (const ResourceNotFoundException& stex) {
log_warning("ResourceNotFoundException: {}", EX(stex));
return nullptr;
} catch (const std::runtime_error& ex) {
log_error("Exception: {}", EX(ex));
return nullptr;
} catch (const std::logic_error& ex) {
log_error("Exception: logic error {}", EX(ex));
return nullptr;
}
}
int Server::ReadCallback(
UpnpWebFileHandle fileHandle,
char* buf,
std::size_t length,
const void* cookie,
const void* requestCookie)
{
log_debug("{} read({})", fileHandle, length);
if (static_cast<const Server*>(cookie)->getShutdownStatus())
return GRB_READ_ERROR;
auto client = requestCookie ? static_cast<const ClientObservation*>(requestCookie) : nullptr;
auto quirks = client ? std::make_shared<Quirks>(client) : nullptr;
if (quirks && !quirks->isAllowed()) {
auto clientIp = client && client->addr ? client->addr->getNameInfo(false) : "unknown";
log_debug("Client blocked {}", clientIp);
return GRB_READ_ERROR;
}
auto ioHandler = static_cast<IOHandler*>(fileHandle);
return ioHandler ? ioHandler->read(reinterpret_cast<std::byte*>(buf), length) : GRB_READ_END;
}
int Server::WriteCallback(
UpnpWebFileHandle fileHandle,
char* buf,
std::size_t length,
const void* cookie,
const void* requestCookie)
{
log_debug("{} write not implemented({})", fileHandle, length);
return 0;
}
int Server::SeekCallback(
UpnpWebFileHandle fileHandle,
off_t offset,
int whence,
const void* cookie,
const void* requestCookie)
{
log_debug("{} seek({}, {})", fileHandle, offset, whence);
try {
auto client = requestCookie ? static_cast<const ClientObservation*>(requestCookie) : nullptr;
auto quirks = client ? std::make_shared<Quirks>(client) : nullptr;
if (quirks && !quirks->isAllowed()) {
auto clientIp = client && client->addr ? client->addr->getNameInfo(false) : "unknown";
log_debug("Client blocked {}", clientIp);
return -1;
}
auto ioHandler = static_cast<IOHandler*>(fileHandle);
if (ioHandler)
ioHandler->seek(offset, whence);
return 0;
} catch (const std::runtime_error& e) {
log_error("Exception during seek: {}", EX(e));
return -1;
} catch (const std::logic_error& ex) {
log_error("Exception during seek: logic error {}", EX(ex));
return -1;
}
}
int Server::CloseCallback(
UpnpWebFileHandle fileHandle,
const void* cookie,
const void* requestCookie)
{
log_debug("{} close()", fileHandle);
int retClose = 0;
auto ioHandler = std::unique_ptr<IOHandler>(static_cast<IOHandler*>(fileHandle));
if (ioHandler) {
try {
ioHandler->close();
} catch (const std::runtime_error& e) {
log_error("Exception during close: {}", EX(e));
retClose = -1;
} catch (const std::logic_error& ex) {
log_error("Exception during close: logic error {}", EX(ex));
retClose = -1;
}
}
return retClose;
}
int Server::registerVirtualDirCallbacks()
{
// UpnpVirtualDir GetInfoCallback
log_debug("Setting UpnpVirtualDir GetInfoCallback");
int ret = UpnpVirtualDir_set_GetInfoCallback(GetInfoCallback);
if (ret != UPNP_E_SUCCESS)
return ret;
// UpnpVirtualDir OpenCallback
log_debug("Setting UpnpVirtualDir OpenCallback");
ret = UpnpVirtualDir_set_OpenCallback(OpenCallback);
if (ret != UPNP_E_SUCCESS)
return ret;
// UpnpVirtualDir ReadCallback
log_debug("Setting UpnpVirtualDir ReadCallback");
ret = UpnpVirtualDir_set_ReadCallback(ReadCallback);
if (ret != UPNP_E_SUCCESS)
return ret;
// UpnpVirtualDir WriteCallback
log_debug("Setting UpnpVirtualDir WriteCallback");
ret = UpnpVirtualDir_set_WriteCallback(WriteCallback);
if (ret != UPNP_E_SUCCESS)
return ret;
// UpnpVirtualDir SeekCallback
log_debug("Setting UpnpVirtualDir SeekCallback");
ret = UpnpVirtualDir_set_SeekCallback(SeekCallback);
if (ret != UPNP_E_SUCCESS)
return ret;
// UpnpVirtualDir CloseCallback"
log_debug("Setting UpnpVirtualDir CloseCallback");
ret = UpnpVirtualDir_set_CloseCallback(CloseCallback);
return ret;
}