/*****************************************************************************
* upnp_server.cpp : UPnP server module
*****************************************************************************
* Copyright © 2024 VLC authors and VideoLAN
*
* Authors: Hamza Parnica <hparnica@gmail.com>
* Alaric Senat <alaric@videolabs.io>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <atomic>
#include <cstring>
#include <services_discovery/upnp-wrapper.hpp>
#include <vlc_common.h>
#include <vlc_addons.h>
#include <vlc_interface.h>
#include <vlc_rand.h>
#include "FileHandler.hpp"
#include "cds/Container.hpp"
#include "cds/cds.hpp"
#include "ml.hpp"
#include "upnp_server.hpp"
#include "utils.hpp"
#include "xml_wrapper.hpp"
#define CDS_ID "urn:upnp-org:serviceId:ContentDirectory"
#define CMS_ID "urn:upnp-org:serviceId:ConnectionManager"
#define UPNP_SERVICE_TYPE(service) "urn:schemas-upnp-org:service:" service ":1"
struct intf_sys_t
{
ml::MediaLibraryContext p_ml;
std::unique_ptr<vlc_ml_event_callback_t, std::function< void(vlc_ml_event_callback_t *)>>
ml_callback_handle;
std::unique_ptr<char, std::function<void(void *)>> uuid;
UpnpDevice_Handle p_device_handle;
std::unique_ptr<UpnpInstanceWrapper, std::function<void(UpnpInstanceWrapper *)>> upnp;
// This integer is atomically incremented at each medialib modification. It will be sent in each
// response. If the client notice that the update id has been incremented since the last
// request, he knows that the server state has changed and hence can refetch the exposed
// hierarchy.
std::atomic<unsigned int> upnp_update_id;
std::vector<std::unique_ptr<cds::Object>> obj_hierarchy;
bool extra_verbose;
};
static void medialibrary_event_callback(void *p_data, const struct vlc_ml_event_t *p_event)
{
intf_thread_t *p_intf = (intf_thread_t *)p_data;
intf_sys_t *p_sys = p_intf->p_sys;
switch (p_event->i_type)
{
case VLC_ML_EVENT_MEDIA_ADDED:
case VLC_ML_EVENT_MEDIA_UPDATED:
case VLC_ML_EVENT_MEDIA_DELETED:
case VLC_ML_EVENT_ARTIST_ADDED:
case VLC_ML_EVENT_ARTIST_UPDATED:
case VLC_ML_EVENT_ARTIST_DELETED:
case VLC_ML_EVENT_ALBUM_ADDED:
case VLC_ML_EVENT_ALBUM_UPDATED:
case VLC_ML_EVENT_ALBUM_DELETED:
case VLC_ML_EVENT_PLAYLIST_ADDED:
case VLC_ML_EVENT_PLAYLIST_UPDATED:
case VLC_ML_EVENT_PLAYLIST_DELETED:
case VLC_ML_EVENT_GENRE_ADDED:
case VLC_ML_EVENT_GENRE_UPDATED:
case VLC_ML_EVENT_GENRE_DELETED:
p_sys->upnp_update_id++;
break;
}
}
static bool cds_browse(UpnpActionRequest *request, intf_thread_t *intf)
{
intf_sys_t *sys = intf->p_sys;
auto *action_rq =
reinterpret_cast<IXML_Element *>(UpnpActionRequest_get_ActionRequest(request));
const char *object_id = xml_getChildElementValue(action_rq, "ObjectID");
if (object_id == nullptr)
return false;
const char *browse_flag = xml_getChildElementValue(action_rq, "BrowseFlag");
const char *starting_index = xml_getChildElementValue(action_rq, "StartingIndex");
const char *requested_count = xml_getChildElementValue(action_rq, "RequestedCount");
if (browse_flag == nullptr || starting_index == nullptr || requested_count == nullptr)
return false;
cds::Container::BrowseParams browse_params{
static_cast<uint32_t>(strtoul(starting_index, nullptr, 10)),
static_cast<uint32_t>(strtoul(requested_count, nullptr, 10)),
};
xml::Document result;
xml::Element didl_lite = result.create_element("DIDL-Lite");
// Standard upnp attributes
didl_lite.set_attribute("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
didl_lite.set_attribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
didl_lite.set_attribute("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
const std::string id = object_id;
unsigned obj_idx;
cds::Object::ExtraId extra_id;
try
{
std::tie(obj_idx, extra_id) = cds::parse_id(id);
}
catch (const std::invalid_argument &e)
{
UpnpActionRequest_set_ErrCode(request, 500);
return false;
}
std::string str_nb_returned;
std::string str_total_matches;
const cds::Object &obj = *sys->obj_hierarchy[obj_idx];
try
{
if (strcmp(browse_flag, "BrowseDirectChildren") == 0 &&
obj.type == cds::Object::Type::Container)
{
const auto browse_stats =
static_cast<const cds::Container &>(obj).browse_direct_children(
didl_lite, browse_params, extra_id);
str_nb_returned = std::to_string(browse_stats.result_count);
str_total_matches = std::to_string(browse_stats.total_matches);
}
else if (strcmp(browse_flag, "BrowseMetadata") == 0)
{
didl_lite.add_child(obj.browse_metadata(result, extra_id));
str_nb_returned = "1";
str_total_matches = "1";
}
} catch (const ml::errors::ForbiddenAccess&)
{
msg_Warn(intf, "Client tried to browse a private medialibrary element.");
UpnpActionRequest_set_ErrCode(request, 404);
return false;
} catch (const ml::errors::UnknownObject&)
{
UpnpActionRequest_set_ErrCode(request, 404);
return false;
}
result.set_entry(std::move(didl_lite));
const char *action_name = UpnpActionRequest_get_ActionName_cstr(request);
const auto up_reponse_str = result.to_wrapped_cstr();
const auto str_update_id = std::to_string(sys->upnp_update_id.load());
auto *p_answer = UpnpActionRequest_get_ActionResult(request);
static constexpr char service_type[] = UPNP_SERVICE_TYPE("ContentDirectory");
UpnpAddToActionResponse(&p_answer, action_name, service_type, "Result", up_reponse_str.get());
UpnpAddToActionResponse(
&p_answer, action_name, service_type, "NumberReturned", str_nb_returned.c_str());
UpnpAddToActionResponse(
&p_answer, action_name, service_type, "TotalMatches", str_total_matches.c_str());
UpnpAddToActionResponse(
&p_answer, action_name, service_type, "UpdateID", str_update_id.c_str());
UpnpActionRequest_set_ActionResult(request, p_answer);
if (sys->extra_verbose)
msg_Dbg(intf, "Sending response to client: \n%s", up_reponse_str.get());
return true;
}
static void handle_action_request(UpnpActionRequest *p_request, intf_thread_t *p_intf)
{
intf_sys_t *sys = p_intf->p_sys;
IXML_Document *action_result = nullptr;
const char *service_id = UpnpActionRequest_get_ServiceID_cstr(p_request);
const char *action_name = UpnpActionRequest_get_ActionName_cstr(p_request);
const auto client_addr = utils::addr_to_string(UpnpActionRequest_get_CtrlPtIPAddr(p_request));
msg_Dbg(p_intf,
"Received action request \"%s\" for service \"%s\" from %s",
action_name,
service_id,
client_addr.c_str());
if (strcmp(service_id, CMS_ID) == 0)
{
static constexpr char cms_type[] = UPNP_SERVICE_TYPE("ConnectionManager");
if (strcmp(action_name, "GetProtocolInfo") == 0)
{
UpnpAddToActionResponse(
&action_result, action_name, cms_type, "Source", "http-get:*:*:*");
UpnpAddToActionResponse(&action_result, action_name, cms_type, "Sink", "");
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
else if (strcmp(action_name, "GetCurrentConnectionIDs") == 0)
{
UpnpAddToActionResponse(&action_result, action_name, cms_type, "ConnectionIDs", "");
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
}
else if (strcmp(service_id, CDS_ID) == 0)
{
static constexpr char cds_type[] = UPNP_SERVICE_TYPE("ContentDirectory");
if (strcmp(action_name, "Browse") == 0)
{
if (!cds_browse(p_request, p_intf))
msg_Err(p_intf, "Failed to respond to browse action request");
}
else if (strcmp(action_name, "GetSearchCapabilities") == 0)
{
UpnpAddToActionResponse(&action_result, action_name, cds_type, "SearchCaps", "");
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
else if (strcmp(action_name, "GetSortCapabilities") == 0)
{
UpnpAddToActionResponse(&action_result, action_name, cds_type, "SortCaps", "");
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
else if (strcmp(action_name, "GetSystemUpdateID") == 0)
{
char *psz_update_id;
if (asprintf(&psz_update_id, "%d", sys->upnp_update_id.load()) == -1)
return;
auto up_update_id = vlc::wrap_cptr(psz_update_id);
UpnpAddToActionResponse(&action_result, action_name, cds_type, "Id", psz_update_id);
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
}
else if (strcmp(service_id, "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar") == 0)
{
// We need a mockup of this service to support Microsoft devices and clients.
if (strcmp(action_name, "IsAuthorized") == 0)
{
UpnpAddToActionResponse(&action_result, action_name, CDS_ID, "Result", "1");
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
else if (strcmp(action_name, "IsValidated") == 0)
{
UpnpAddToActionResponse(&action_result, action_name, CDS_ID, "Result", "1");
UpnpActionRequest_set_ActionResult(p_request, action_result);
}
}
}
static int Callback(Upnp_EventType event_type, UpnpEventPtr event, void *cookie)
{
auto *intf = static_cast<intf_thread_t *>(cookie);
switch (event_type)
{
case UPNP_CONTROL_ACTION_REQUEST:
{
msg_Dbg(intf, "Action request");
// We need to const_cast here because the upnp callback has to take a const void* for
// the event data even if the sdk also expect us to modify it sometimes (in the case of
// a upnp response for example)
auto *rq =
const_cast<UpnpActionRequest *>(static_cast<const UpnpActionRequest *>(event));
handle_action_request(rq, intf);
}
break;
case UPNP_CONTROL_GET_VAR_REQUEST:
msg_Dbg(intf, "Var request");
break;
case UPNP_EVENT_SUBSCRIPTION_REQUEST:
msg_Dbg(intf, "Sub request");
break;
default:
msg_Err(intf, "Unhandled event: %d", event_type);
return UPNP_E_INVALID_ACTION;
}
return UPNP_E_SUCCESS;
}
// UPNP Callbacks
static int
getinfo_cb(const char *url, UpnpFileInfo *info, const void *void_intf, const void **fhandler)
{
intf_thread_t *intf = (intf_thread_t *)(void_intf);
const char *user_agent = UpnpFileInfo_get_Os_cstr(info);
if (user_agent == nullptr)
return UPNP_E_BAD_REQUEST;
msg_Dbg(intf, "GetInfo callback on: \"%s\" from: \"%s\"", url, user_agent);
UpnpFileInfo_set_IsReadable(info, true);
UpnpFileInfo_set_IsDirectory(info, false);
std::unique_ptr<FileHandler> file_handler;
try
{
file_handler = parse_url(url, intf->p_sys->p_ml);
}
catch (const std::exception &)
{
return UPNP_E_FILE_NOT_FOUND;
}
if (file_handler == nullptr)
return UPNP_E_FILE_NOT_FOUND;
file_handler->get_info(*info);
// Pass the filehandler ownership to the open callback to avoid reparsing the url
*fhandler = static_cast<void*>(file_handler.release());
return UPNP_E_SUCCESS;
}
static UpnpWebFileHandle
open_cb(const char *url, enum UpnpOpenFileMode, const void *void_intf, const void *file_handler)
{
intf_thread_t *intf = (intf_thread_t *)(void_intf);
msg_Dbg(intf, "Opening: %s", url);
FileHandler *ret = (FileHandler *)(file_handler);
if (!ret)
{
try
{
ret = parse_url(url, intf->p_sys->p_ml).release();
}
catch (const std::exception &)
{
return nullptr;
}
}
if (ret == nullptr)
return nullptr;
if (!ret->open(VLC_OBJECT(intf)))
{
msg_Err(intf, "Failed to open %s", url);
delete ret;
return nullptr;
}
return ret;
}
static int
read_cb(UpnpWebFileHandle fileHnd, char *buf, size_t buflen, const void *void_intf, const void *)
{
intf_thread_t *intf = (intf_thread_t *)(void_intf);
assert(fileHnd);
auto *impl = static_cast<FileHandler *>(fileHnd);
const size_t bytes_read = impl->read((uint8_t*)buf, buflen);
msg_Dbg(intf, "http read callback, %zub requested %zub returned", buflen, bytes_read);
return bytes_read;
}
static int
seek_cb(UpnpWebFileHandle fileHnd, off_t offset, int origin, const void *void_intf, const void *)
{
intf_thread_t *intf = (intf_thread_t *)(void_intf);
assert(fileHnd);
msg_Dbg(
intf, "http seek callback offset: %jd origin: %d", static_cast<intmax_t>(offset), origin);
auto *impl = static_cast<FileHandler *>(fileHnd);
const auto seek_type = static_cast<FileHandler::SeekType>(origin);
const bool success = impl->seek(seek_type, offset);
return success == true ? 0 : -1;
}
static int close_cb(UpnpWebFileHandle fileHnd, const void *void_intf, const void *)
{
intf_thread_t *intf = (intf_thread_t *)(void_intf);
assert(fileHnd);
msg_Dbg(intf, "http close callback");
delete static_cast<FileHandler *>(fileHnd);
return 0;
}
static xml::Document make_server_identity(const char *uuid, const char *server_name)
{
xml::Document ret;
const auto icon_elem =
[&ret](const char *url, const char *width, const char *height) -> xml::Element {
return ret.create_element("icon",
ret.create_element("mimetype", ret.create_text_node("image/png")),
ret.create_element("width", ret.create_text_node(width)),
ret.create_element("height", ret.create_text_node(height)),
ret.create_element("depth", ret.create_text_node("8")),
ret.create_element("url", ret.create_text_node(url)));
};
const auto service_elem = [&ret](const char *service) -> xml::Element {
const auto type = std::string("urn:schemas-upnp-org:service:") + service + ":1";
const auto id = std::string("urn:upnp-org:serviceId:") + service;
const auto scpd_url = std::string("/") + service + ".xml";
const auto control_url = std::string("/") + service + "/Control";
const auto event_url = std::string("/") + service + "/Event";
return ret.create_element(
"service",
ret.create_element("serviceType", ret.create_text_node(type.c_str())),
ret.create_element("serviceId", ret.create_text_node(id.c_str())),
ret.create_element("SCPDURL", ret.create_text_node(scpd_url.c_str())),
ret.create_element("controlURL", ret.create_text_node(control_url.c_str())),
ret.create_element("eventSubURL", ret.create_text_node(event_url.c_str())));
};
const std::string url = utils::get_server_url();
const auto uuid_attr = std::string("uuid:") + uuid;
xml::Element dlna_doc = ret.create_element("dlna:X_DLNADOC", ret.create_text_node("DMS-1.50"));
dlna_doc.set_attribute("xmlns:dlna", "urn:schemas-dlna-org:device-1-0");
xml::Element root = ret.create_element(
"root",
ret.create_element("specVersion",
ret.create_element("major", ret.create_text_node("1")),
ret.create_element("minor", ret.create_text_node("0"))),
ret.create_element(
"device",
std::move(dlna_doc),
ret.create_element("deviceType",
ret.create_text_node("urn:schemas-upnp-org:device:MediaServer:1")),
ret.create_element("presentationUrl", ret.create_text_node(url.c_str())),
ret.create_element("friendlyName", ret.create_text_node(server_name)),
ret.create_element("manufacturer", ret.create_text_node("VideoLAN")),
ret.create_element("manufacturerURL", ret.create_text_node("https://videolan.org")),
ret.create_element("modelDescription", ret.create_text_node("VLC UPNP Media Server")),
ret.create_element("modelName", ret.create_text_node("VLC")),
ret.create_element("modelNumber", ret.create_text_node(PACKAGE_VERSION)),
ret.create_element("modelURL", ret.create_text_node("https://videolan.org/vlc/")),
ret.create_element("serialNumber", ret.create_text_node("1")),
ret.create_element("UDN", ret.create_text_node(uuid_attr.c_str())),
ret.create_element("iconList",
icon_elem("/vlc.png", "32", "32"),
icon_elem("/vlc512x512.png", "512", "512")),
ret.create_element(
"serviceList",
service_elem("ConnectionManager"),
service_elem("ContentDirectory"),
ret.create_element(
"service",
ret.create_element(
"serviceType",
ret.create_text_node(
"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1")),
ret.create_element(
"serviceId",
ret.create_text_node(
"urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar")),
ret.create_element("SCPDURL",
ret.create_text_node("/X_MS_MediaReceiverRegistrar.xml")),
ret.create_element(
"controlURL", ret.create_text_node("/X_MS_MediaReceiverRegistrar/Control")),
ret.create_element(
"eventSubURL",
ret.create_text_node("/X_MS_MediaReceiverRegistrar/Event"))))));
root.set_attribute("xmlns", "urn:schemas-upnp-org:device-1-0");
ret.set_entry(std::move(root));
return ret;
}
static bool init_upnp(intf_thread_t *intf)
{
intf_sys_t *sys = intf->p_sys;
addon_uuid_t uuid;
vlc_rand_bytes(uuid, sizeof(uuid));
sys->uuid = vlc::wrap_cptr(addons_uuid_to_psz(&uuid), &free);
int res = UpnpEnableWebserver(true);
if (res != UPNP_E_SUCCESS)
{
msg_Err(intf, "Enabling libupnp webserver failed: %s", UpnpGetErrorMessage(res));
return false;
}
msg_Info(intf, "Upnp server enabled on %s:%d", UpnpGetServerIpAddress(), UpnpGetServerPort());
const auto str_rootdir = utils::get_root_dir();
res = UpnpSetWebServerRootDir(str_rootdir.c_str());
msg_Dbg(intf, "Webserver root dir set to: \"%s\"", str_rootdir.c_str());
if (res != UPNP_E_SUCCESS)
{
msg_Err(intf, "Setting webserver root dir failed: %s", UpnpGetErrorMessage(res));
UpnpEnableWebserver(false);
return false;
}
const auto server_name = vlc::wrap_cptr(var_InheritString(intf, SERVER_PREFIX "name"), &free);
assert(server_name);
const auto presentation_doc = make_server_identity(sys->uuid.get(), server_name.get());
const auto up_presentation_str = presentation_doc.to_wrapped_cstr();
if (sys->extra_verbose)
msg_Dbg(intf, "%s", up_presentation_str.get());
res = UpnpRegisterRootDevice2(UPNPREG_BUF_DESC,
up_presentation_str.get(),
strlen(up_presentation_str.get()),
1,
Callback,
intf,
&sys->p_device_handle);
if (res != UPNP_E_SUCCESS)
{
msg_Err(intf, "Registration failed: %s", UpnpGetErrorMessage(res));
UpnpEnableWebserver(false);
return false;
}
UpnpVirtualDir_set_GetInfoCallback(reinterpret_cast<VDCallback_GetInfo>(getinfo_cb));
UpnpVirtualDir_set_OpenCallback(reinterpret_cast<VDCallback_Open>(open_cb));
UpnpVirtualDir_set_ReadCallback(reinterpret_cast<VDCallback_Read>(read_cb));
UpnpVirtualDir_set_SeekCallback(reinterpret_cast<VDCallback_Seek>(seek_cb));
UpnpVirtualDir_set_CloseCallback(reinterpret_cast<VDCallback_Close>(close_cb));
UpnpAddVirtualDir("/media", intf, nullptr);
UpnpAddVirtualDir("/thumbnail", intf, nullptr);
UpnpAddVirtualDir("/subtitle", intf, nullptr);
res = UpnpSendAdvertisement(sys->p_device_handle, 1800);
if (res != UPNP_E_SUCCESS)
{
msg_Dbg(intf, "Advertisement failed: %s", UpnpGetErrorMessage(res));
UpnpUnRegisterRootDevice(sys->p_device_handle);
UpnpEnableWebserver(false);
return false;
}
sys->upnp_update_id = 0;
return true;
}
namespace Server
{
int open(vlc_object_t *p_this)
{
intf_thread_t *intf = container_of(p_this, intf_thread_t, obj);
intf_sys_t *sys = new (std::nothrow) intf_sys_t;
if (unlikely(sys == nullptr))
return VLC_ENOMEM;
intf->p_sys = sys;
const bool share_private_media = var_InheritBool(p_this, SERVER_PREFIX "share-private-media");
sys->p_ml = ml::MediaLibraryContext{vlc_ml_instance_get(p_this), share_private_media};
if (!sys->p_ml.handle)
{
msg_Err(intf, "Medialibrary not initialized");
delete sys;
return VLC_EGENERIC;
}
vlc_ml_event_callback_t *cb =
vlc_ml_event_register_callback(sys->p_ml.handle, medialibrary_event_callback, intf);
const auto release_cb = [sys](vlc_ml_event_callback_t *cb) {
vlc_ml_event_unregister_callback(sys->p_ml.handle, cb);
};
sys->ml_callback_handle = {cb, release_cb};
sys->upnp = vlc::wrap_cptr(UpnpInstanceWrapper::get(p_this),
[](UpnpInstanceWrapper *p_upnp) { p_upnp->release(); });
sys->obj_hierarchy = cds::init_hierarchy(sys->p_ml);
sys->extra_verbose = var_InheritInteger(p_this, "verbose") >= 4;
if (!init_upnp(intf))
{
delete sys;
return VLC_EGENERIC;
}
return VLC_SUCCESS;
}
void close(vlc_object_t *p_this)
{
intf_thread_t *intf = container_of(p_this, intf_thread_t, obj);
intf_sys_t *sys = intf->p_sys;
UpnpUnRegisterRootDevice(sys->p_device_handle);
UpnpEnableWebserver(false);
delete sys;
}
} // namespace Server
¤ Dauer der Verarbeitung: 0.2 Sekunden
(vorverarbeitet am 2026-11-04)
¤
*© Formatika GbR, Deutschland
|
|