summaryrefslogtreecommitdiffstats
path: root/xbmc/network
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/network')
-rw-r--r--xbmc/network/AirPlayServer.cpp1225
-rw-r--r--xbmc/network/AirPlayServer.h108
-rw-r--r--xbmc/network/AirTunesServer.cpp753
-rw-r--r--xbmc/network/AirTunesServer.h101
-rw-r--r--xbmc/network/CMakeLists.txt60
-rw-r--r--xbmc/network/DNSNameCache.cpp114
-rw-r--r--xbmc/network/DNSNameCache.h34
-rw-r--r--xbmc/network/EventClient.cpp788
-rw-r--r--xbmc/network/EventClient.h260
-rw-r--r--xbmc/network/EventPacket.cpp80
-rw-r--r--xbmc/network/EventPacket.h227
-rw-r--r--xbmc/network/EventServer.cpp352
-rw-r--r--xbmc/network/EventServer.h81
-rw-r--r--xbmc/network/GUIDialogNetworkSetup.cpp471
-rw-r--r--xbmc/network/GUIDialogNetworkSetup.h74
-rw-r--r--xbmc/network/IWSDiscovery.h25
-rw-r--r--xbmc/network/Network.cpp525
-rw-r--r--xbmc/network/Network.h123
-rw-r--r--xbmc/network/NetworkServices.cpp1267
-rw-r--r--xbmc/network/NetworkServices.h130
-rw-r--r--xbmc/network/Socket.cpp329
-rw-r--r--xbmc/network/Socket.h253
-rw-r--r--xbmc/network/TCPServer.cpp756
-rw-r--r--xbmc/network/TCPServer.h118
-rw-r--r--xbmc/network/UdpClient.cpp262
-rw-r--r--xbmc/network/UdpClient.h69
-rw-r--r--xbmc/network/WakeOnAccess.cpp956
-rw-r--r--xbmc/network/WakeOnAccess.h81
-rw-r--r--xbmc/network/WebServer.cpp1409
-rw-r--r--xbmc/network/WebServer.h131
-rw-r--r--xbmc/network/Zeroconf.cpp187
-rw-r--r--xbmc/network/Zeroconf.h138
-rw-r--r--xbmc/network/ZeroconfBrowser.cpp245
-rw-r--r--xbmc/network/ZeroconfBrowser.h174
-rw-r--r--xbmc/network/cddb.cpp1083
-rw-r--r--xbmc/network/cddb.h125
-rw-r--r--xbmc/network/dacp/CMakeLists.txt5
-rw-r--r--xbmc/network/dacp/dacp.cpp97
-rw-r--r--xbmc/network/dacp/dacp.h38
-rw-r--r--xbmc/network/httprequesthandler/CMakeLists.txt30
-rw-r--r--xbmc/network/httprequesthandler/HTTPFileHandler.cpp103
-rw-r--r--xbmc/network/httprequesthandler/HTTPFileHandler.h48
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageHandler.cpp51
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageHandler.h29
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp170
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h46
-rw-r--r--xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp183
-rw-r--r--xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h71
-rw-r--r--xbmc/network/httprequesthandler/HTTPPythonHandler.cpp250
-rw-r--r--xbmc/network/httprequesthandler/HTTPPythonHandler.h51
-rw-r--r--xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp85
-rw-r--r--xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h30
-rw-r--r--xbmc/network/httprequesthandler/HTTPVfsHandler.cpp106
-rw-r--r--xbmc/network/httprequesthandler/HTTPVfsHandler.h28
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp60
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h38
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp132
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h32
-rw-r--r--xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp152
-rw-r--r--xbmc/network/httprequesthandler/IHTTPRequestHandler.h247
-rw-r--r--xbmc/network/httprequesthandler/python/CMakeLists.txt10
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp73
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h32
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonRequest.h42
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp458
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h47
-rw-r--r--xbmc/network/mdns/CMakeLists.txt9
-rw-r--r--xbmc/network/mdns/ZeroconfBrowserMDNS.cpp420
-rw-r--r--xbmc/network/mdns/ZeroconfBrowserMDNS.h92
-rw-r--r--xbmc/network/mdns/ZeroconfMDNS.cpp245
-rw-r--r--xbmc/network/mdns/ZeroconfMDNS.h71
-rw-r--r--xbmc/network/test/CMakeLists.txt5
-rw-r--r--xbmc/network/test/TestWebServer.cpp926
-rw-r--r--xbmc/network/test/data/webserver/test-ranges.txt1
-rw-r--r--xbmc/network/test/data/webserver/test.html1
-rw-r--r--xbmc/network/test/data/webserver/test.pngbin0 -> 634 bytes
-rw-r--r--xbmc/network/upnp/CMakeLists.txt18
-rw-r--r--xbmc/network/upnp/UPnP.cpp891
-rw-r--r--xbmc/network/upnp/UPnP.h108
-rw-r--r--xbmc/network/upnp/UPnPInternal.cpp1245
-rw-r--r--xbmc/network/upnp/UPnPInternal.h122
-rw-r--r--xbmc/network/upnp/UPnPPlayer.cpp625
-rw-r--r--xbmc/network/upnp/UPnPPlayer.h77
-rw-r--r--xbmc/network/upnp/UPnPRenderer.cpp758
-rw-r--r--xbmc/network/upnp/UPnPRenderer.h73
-rw-r--r--xbmc/network/upnp/UPnPServer.cpp1388
-rw-r--r--xbmc/network/upnp/UPnPServer.h159
-rw-r--r--xbmc/network/upnp/UPnPSettings.cpp110
-rw-r--r--xbmc/network/upnp/UPnPSettings.h56
-rw-r--r--xbmc/network/websocket/CMakeLists.txt11
-rw-r--r--xbmc/network/websocket/WebSocket.cpp430
-rw-r--r--xbmc/network/websocket/WebSocket.h136
-rw-r--r--xbmc/network/websocket/WebSocketManager.cpp77
-rw-r--r--xbmc/network/websocket/WebSocketManager.h19
-rw-r--r--xbmc/network/websocket/WebSocketV13.cpp154
-rw-r--r--xbmc/network/websocket/WebSocketV13.h22
-rw-r--r--xbmc/network/websocket/WebSocketV8.cpp189
-rw-r--r--xbmc/network/websocket/WebSocketV8.h33
98 files changed, 24329 insertions, 0 deletions
diff --git a/xbmc/network/AirPlayServer.cpp b/xbmc/network/AirPlayServer.cpp
new file mode 100644
index 0000000..74251d3
--- /dev/null
+++ b/xbmc/network/AirPlayServer.cpp
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * Many concepts and protocol specification in this code are taken
+ * from Airplayer. https://github.com/PascalW/Airplayer
+ */
+
+#include "AirPlayServer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#ifdef HAS_ZEROCONF
+#include "network/Zeroconf.h"
+#endif // HAS_ZEROCONF
+
+#include <inttypes.h>
+
+#include <plist/plist.h>
+
+using KODI::UTILITY::CDigest;
+using namespace std::chrono_literals;
+
+#ifdef TARGET_WINDOWS
+#define close closesocket
+#endif
+
+#define RECEIVEBUFFER 1024
+
+#define AIRPLAY_STATUS_OK 200
+#define AIRPLAY_STATUS_SWITCHING_PROTOCOLS 101
+#define AIRPLAY_STATUS_NEED_AUTH 401
+#define AIRPLAY_STATUS_NOT_FOUND 404
+#define AIRPLAY_STATUS_METHOD_NOT_ALLOWED 405
+#define AIRPLAY_STATUS_PRECONDITION_FAILED 412
+#define AIRPLAY_STATUS_NOT_IMPLEMENTED 501
+#define AIRPLAY_STATUS_NO_RESPONSE_NEEDED 1000
+
+CCriticalSection CAirPlayServer::ServerInstanceLock;
+CAirPlayServer *CAirPlayServer::ServerInstance = NULL;
+int CAirPlayServer::m_isPlaying = 0;
+
+#define EVENT_NONE -1
+#define EVENT_PLAYING 0
+#define EVENT_PAUSED 1
+#define EVENT_LOADING 2
+#define EVENT_STOPPED 3
+const char *eventStrings[] = {"playing", "paused", "loading", "stopped"};
+
+#define PLAYBACK_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>duration</key>\r\n" \
+ "<real>{:f}</real>\r\n" \
+ "<key>loadedTimeRanges</key>\r\n" \
+ "<array>\r\n" \
+ "\t\t<dict>\r\n" \
+ "\t\t\t<key>duration</key>\r\n" \
+ "\t\t\t<real>{:f}</real>\r\n" \
+ "\t\t\t<key>start</key>\r\n" \
+ "\t\t\t<real>0.0</real>\r\n" \
+ "\t\t</dict>\r\n" \
+ "</array>\r\n" \
+ "<key>playbackBufferEmpty</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>playbackBufferFull</key>\r\n" \
+ "<false/>\r\n" \
+ "<key>playbackLikelyToKeepUp</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>position</key>\r\n" \
+ "<real>{:f}</real>\r\n" \
+ "<key>rate</key>\r\n" \
+ "<real>{:d}</real>\r\n" \
+ "<key>readyToPlay</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>seekableTimeRanges</key>\r\n" \
+ "<array>\r\n" \
+ "\t\t<dict>\r\n" \
+ "\t\t\t<key>duration</key>\r\n" \
+ "\t\t\t<real>{:f}</real>\r\n" \
+ "\t\t\t<key>start</key>\r\n" \
+ "\t\t\t<real>0.0</real>\r\n" \
+ "\t\t</dict>\r\n" \
+ "</array>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define PLAYBACK_INFO_NOT_READY "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
+"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
+"<plist version=\"1.0\">\r\n"\
+"<dict>\r\n"\
+"<key>readyToPlay</key>\r\n"\
+"<false/>\r\n"\
+"</dict>\r\n"\
+"</plist>\r\n"
+
+#define SERVER_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>deviceid</key>\r\n" \
+ "<string>{:s}</string>\r\n" \
+ "<key>features</key>\r\n" \
+ "<integer>119</integer>\r\n" \
+ "<key>model</key>\r\n" \
+ "<string>Kodi,1</string>\r\n" \
+ "<key>protovers</key>\r\n" \
+ "<string>1.0</string>\r\n" \
+ "<key>srcvers</key>\r\n" \
+ "<string>" AIRPLAY_SERVER_VERSION_STR "</string>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define EVENT_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>category</key>\r\n" \
+ "<string>video</string>\r\n" \
+ "<key>sessionID</key>\r\n" \
+ "<integer>{:d}</integer>\r\n" \
+ "<key>state</key>\r\n" \
+ "<string>{:s}</string>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define AUTH_REALM "AirPlay"
+#define AUTH_REQUIRED "WWW-Authenticate: Digest realm=\"" AUTH_REALM "\", nonce=\"{:s}\"\r\n"
+
+void CAirPlayServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ // We are only interested in player changes
+ if ((flag & ANNOUNCEMENT::Player) == 0)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ if (sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER && ServerInstance)
+ {
+ if (message == "OnStop")
+ {
+ bool shouldRestoreVolume = true;
+ if (data.isMember("player") && data["player"].isMember("playerid"))
+ shouldRestoreVolume = (data["player"]["playerid"] != PLAYLIST::TYPE_PICTURE);
+
+ if (shouldRestoreVolume)
+ restoreVolume();
+
+ ServerInstance->AnnounceToClients(EVENT_STOPPED);
+ }
+ else if (message == "OnPlay" || message == "OnResume")
+ {
+ ServerInstance->AnnounceToClients(EVENT_PLAYING);
+ }
+ else if (message == "OnPause")
+ {
+ ServerInstance->AnnounceToClients(EVENT_PAUSED);
+ }
+ }
+}
+
+bool CAirPlayServer::StartServer(int port, bool nonlocal)
+{
+ StopServer(true);
+
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ ServerInstance = new CAirPlayServer(port, nonlocal);
+ if (ServerInstance->Initialize())
+ {
+ ServerInstance->Create();
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CAirPlayServer::SetCredentials(bool usePassword, const std::string& password)
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+ bool ret = false;
+
+ if (ServerInstance)
+ {
+ ret = ServerInstance->SetInternalCredentials(usePassword, password);
+ }
+ return ret;
+}
+
+bool CAirPlayServer::SetInternalCredentials(bool usePassword, const std::string& password)
+{
+ m_usePassword = usePassword;
+ m_password = password;
+ return true;
+}
+
+void ClearPhotoAssetCache()
+{
+ CLog::Log(LOGINFO, "AIRPLAY: Cleaning up photoassetcache");
+ // remove all cached photos
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory("special://temp/", items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+ if (!pItem->m_bIsFolder)
+ {
+ if (StringUtils::StartsWithNoCase(pItem->GetLabel(), "airplayasset") &&
+ (StringUtils::EndsWithNoCase(pItem->GetLabel(), ".jpg") ||
+ StringUtils::EndsWithNoCase(pItem->GetLabel(), ".png") ))
+ {
+ XFILE::CFile::Delete(pItem->GetPath());
+ }
+ }
+ }
+}
+
+void CAirPlayServer::StopServer(bool bWait)
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+ //clean up the photo cache temp folder
+ ClearPhotoAssetCache();
+
+ if (ServerInstance)
+ {
+ ServerInstance->StopThread(bWait);
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+ }
+}
+
+bool CAirPlayServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return static_cast<CThread*>(ServerInstance)->IsRunning();
+}
+
+void CAirPlayServer::AnnounceToClients(int state)
+{
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+
+ for (auto& it : m_connections)
+ {
+ std::string reverseHeader;
+ std::string reverseBody;
+ std::string response;
+ int reverseSocket = INVALID_SOCKET;
+ it.ComposeReverseEvent(reverseHeader, reverseBody, state);
+
+ // Send event status per reverse http socket (play, loading, paused)
+ // if we have a reverse header and a reverse socket
+ if (!reverseHeader.empty() && m_reverseSockets.find(it.m_sessionId) != m_reverseSockets.end())
+ {
+ //search the reverse socket to this sessionid
+ response = StringUtils::Format("POST /event HTTP/1.1\r\n");
+ reverseSocket = m_reverseSockets[it.m_sessionId]; //that is our reverse socket
+ response += reverseHeader;
+ }
+ response += "\r\n";
+
+ if (!reverseBody.empty())
+ {
+ response += reverseBody;
+ }
+
+ // don't send it to the connection object
+ // the reverse socket itself belongs to
+ if (reverseSocket != INVALID_SOCKET && reverseSocket != it.m_socket)
+ {
+ send(reverseSocket, response.c_str(), response.size(), 0);//send the event status on the eventSocket
+ }
+ }
+}
+
+CAirPlayServer::CAirPlayServer(int port, bool nonlocal) : CThread("AirPlayServer")
+{
+ m_port = port;
+ m_nonlocal = nonlocal;
+ m_ServerSockets = std::vector<SOCKET>();
+ m_usePassword = false;
+ m_origVolume = -1;
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+CAirPlayServer::~CAirPlayServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+void handleZeroconfAnnouncement()
+{
+#if defined(HAS_ZEROCONF)
+ static XbmcThreads::EndTime<> timeout(10s);
+ if(timeout.IsTimePast())
+ {
+ CZeroconf::GetInstance()->ForceReAnnounceService("servers.airplay");
+ timeout.Set(10s);
+ }
+#endif
+}
+
+void CAirPlayServer::Process()
+{
+ m_bStop = false;
+ static int sessionCounter = 0;
+
+ while (!m_bStop)
+ {
+ int max_fd = 0;
+ fd_set rfds;
+ struct timeval to = {1, 0};
+ FD_ZERO(&rfds);
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ FD_SET(socket, &rfds);
+ if ((intptr_t)socket > (intptr_t)max_fd)
+ max_fd = socket;
+ }
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ FD_SET(m_connections[i].m_socket, &rfds);
+ if (m_connections[i].m_socket > max_fd)
+ max_fd = m_connections[i].m_socket;
+ }
+
+ int res = select(max_fd+1, &rfds, NULL, NULL, &to);
+ if (res < 0)
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: Select failed");
+ CThread::Sleep(1000ms);
+ Initialize();
+ }
+ else if (res > 0)
+ {
+ for (int i = m_connections.size() - 1; i >= 0; i--)
+ {
+ int socket = m_connections[i].m_socket;
+ if (FD_ISSET(socket, &rfds))
+ {
+ char buffer[RECEIVEBUFFER] = {};
+ int nread = 0;
+ nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
+ if (nread > 0)
+ {
+ std::string sessionId;
+ m_connections[i].PushBuffer(this, buffer, nread, sessionId, m_reverseSockets);
+ }
+ if (nread <= 0)
+ {
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ CLog::Log(LOGINFO, "AIRPLAY Server: Disconnection detected");
+ m_connections[i].Disconnect();
+ m_connections.erase(m_connections.begin() + i);
+ }
+ }
+ }
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ if (FD_ISSET(socket, &rfds))
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY Server: New connection detected");
+ CTCPClient newconnection;
+ newconnection.m_socket = accept(socket, (struct sockaddr*) &newconnection.m_cliaddr, &newconnection.m_addrlen);
+ sessionCounter++;
+ newconnection.m_sessionCounter = sessionCounter;
+
+ if (newconnection.m_socket == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: Accept of new connection failed: {}", errno);
+ if (EBADF == errno)
+ {
+ CThread::Sleep(1000ms);
+ Initialize();
+ break;
+ }
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ CLog::Log(LOGINFO, "AIRPLAY Server: New connection added");
+ m_connections.push_back(newconnection);
+ }
+ }
+ }
+ }
+
+ // by reannouncing the zeroconf service
+ // we fix issues where xbmc is detected
+ // as audio-only target on devices with
+ // ios7 and later
+ handleZeroconfAnnouncement();
+ }
+
+ Deinitialize();
+}
+
+bool CAirPlayServer::Initialize()
+{
+ Deinitialize();
+
+ m_ServerSockets = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "AIRPLAY");
+ if (m_ServerSockets.empty())
+ return false;
+
+ CLog::Log(LOGINFO, "AIRPLAY Server: Successfully initialized");
+ return true;
+}
+
+void CAirPlayServer::Deinitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ m_connections[i].Disconnect();
+
+ m_connections.clear();
+ m_reverseSockets.clear();
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ shutdown(socket, SHUT_RDWR);
+ close(socket);
+ }
+ m_ServerSockets.clear();
+}
+
+CAirPlayServer::CTCPClient::CTCPClient()
+{
+ m_socket = INVALID_SOCKET;
+ m_httpParser = new HttpParser();
+
+ m_addrlen = sizeof(struct sockaddr_storage);
+
+ m_bAuthenticated = false;
+ m_lastEvent = EVENT_NONE;
+}
+
+CAirPlayServer::CTCPClient::CTCPClient(const CTCPClient& client)
+: m_lastEvent(EVENT_NONE)
+{
+ Copy(client);
+ m_httpParser = new HttpParser();
+}
+
+CAirPlayServer::CTCPClient::~CTCPClient()
+{
+ delete m_httpParser;
+}
+
+CAirPlayServer::CTCPClient& CAirPlayServer::CTCPClient::operator=(const CTCPClient& client)
+{
+ Copy(client);
+ m_httpParser = new HttpParser();
+ return *this;
+}
+
+void CAirPlayServer::CTCPClient::PushBuffer(CAirPlayServer *host, const char *buffer,
+ int length, std::string &sessionId, std::map<std::string,
+ int> &reverseSockets)
+{
+ HttpParser::status_t status = m_httpParser->addBytes(buffer, length);
+
+ if (status == HttpParser::Done)
+ {
+ // Parse the request
+ std::string responseHeader;
+ std::string responseBody;
+ int status = ProcessRequest(responseHeader, responseBody);
+ sessionId = m_sessionId;
+ std::string statusMsg = "OK";
+
+ switch(status)
+ {
+ case AIRPLAY_STATUS_NOT_IMPLEMENTED:
+ statusMsg = "Not Implemented";
+ break;
+ case AIRPLAY_STATUS_SWITCHING_PROTOCOLS:
+ statusMsg = "Switching Protocols";
+ reverseSockets[sessionId] = m_socket;//save this socket as reverse http socket for this sessionid
+ break;
+ case AIRPLAY_STATUS_NEED_AUTH:
+ statusMsg = "Unauthorized";
+ break;
+ case AIRPLAY_STATUS_NOT_FOUND:
+ statusMsg = "Not Found";
+ break;
+ case AIRPLAY_STATUS_METHOD_NOT_ALLOWED:
+ statusMsg = "Method Not Allowed";
+ break;
+ case AIRPLAY_STATUS_PRECONDITION_FAILED:
+ statusMsg = "Precondition Failed";
+ break;
+ }
+
+ // Prepare the response
+ std::string response;
+ const time_t ltime = time(NULL);
+ char *date = asctime(gmtime(&ltime)); //Fri, 17 Dec 2010 11:18:01 GMT;
+ date[strlen(date) - 1] = '\0'; // remove \n
+ response = StringUtils::Format("HTTP/1.1 {} {}\nDate: {}\r\n", status, statusMsg, date);
+ if (!responseHeader.empty())
+ {
+ response += responseHeader;
+ }
+
+ response = StringUtils::Format("{}Content-Length: {}\r\n\r\n", response, responseBody.size());
+
+ if (!responseBody.empty())
+ {
+ response += responseBody;
+ }
+
+ // Send the response
+ //don't send response on AIRPLAY_STATUS_NO_RESPONSE_NEEDED
+ if (status != AIRPLAY_STATUS_NO_RESPONSE_NEEDED)
+ {
+ send(m_socket, response.c_str(), response.size(), 0);
+ }
+ // We need a new parser...
+ delete m_httpParser;
+ m_httpParser = new HttpParser;
+ }
+}
+
+void CAirPlayServer::CTCPClient::Disconnect()
+{
+ if (m_socket != INVALID_SOCKET)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ shutdown(m_socket, SHUT_RDWR);
+ close(m_socket);
+ m_socket = INVALID_SOCKET;
+ delete m_httpParser;
+ m_httpParser = NULL;
+ }
+}
+
+void CAirPlayServer::CTCPClient::Copy(const CTCPClient& client)
+{
+ m_socket = client.m_socket;
+ m_cliaddr = client.m_cliaddr;
+ m_addrlen = client.m_addrlen;
+ m_httpParser = client.m_httpParser;
+ m_authNonce = client.m_authNonce;
+ m_bAuthenticated = client.m_bAuthenticated;
+ m_sessionCounter = client.m_sessionCounter;
+}
+
+
+void CAirPlayServer::CTCPClient::ComposeReverseEvent( std::string& reverseHeader,
+ std::string& reverseBody,
+ int state)
+{
+
+ if ( m_lastEvent != state )
+ {
+ switch(state)
+ {
+ case EVENT_PLAYING:
+ case EVENT_LOADING:
+ case EVENT_PAUSED:
+ case EVENT_STOPPED:
+ reverseBody = StringUtils::Format(EVENT_INFO, m_sessionCounter, eventStrings[state]);
+ CLog::Log(LOGDEBUG, "AIRPLAY: sending event: {}", eventStrings[state]);
+ break;
+ }
+ reverseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ reverseHeader =
+ StringUtils::Format("{}Content-Length: {}\r\n", reverseHeader, reverseBody.size());
+ reverseHeader = StringUtils::Format("{}x-apple-session-id: {}\r\n", reverseHeader.c_str(),
+ m_sessionId.c_str());
+ m_lastEvent = state;
+ }
+}
+
+void CAirPlayServer::CTCPClient::ComposeAuthRequestAnswer(std::string& responseHeader, std::string& responseBody)
+{
+ int16_t random=rand();
+ std::string randomStr = std::to_string(random);
+ m_authNonce=CDigest::Calculate(CDigest::Type::MD5, randomStr);
+ responseHeader = StringUtils::Format(AUTH_REQUIRED, m_authNonce);
+ responseBody.clear();
+}
+
+
+//as of rfc 2617
+std::string calcResponse(const std::string& username,
+ const std::string& password,
+ const std::string& realm,
+ const std::string& method,
+ const std::string& digestUri,
+ const std::string& nonce)
+{
+ std::string response;
+ std::string HA1;
+ std::string HA2;
+
+ HA1 = CDigest::Calculate(CDigest::Type::MD5, username + ":" + realm + ":" + password);
+ HA2 = CDigest::Calculate(CDigest::Type::MD5, method + ":" + digestUri);
+ response = CDigest::Calculate(CDigest::Type::MD5, HA1 + ":" + nonce + ":" + HA2);
+ return response;
+}
+
+//helper function
+//from a string field1="value1", field2="value2" it parses the value to a field
+std::string getFieldFromString(const std::string &str, const char* field)
+{
+ std::vector<std::string> tmpAr1 = StringUtils::Split(str, ",");
+ for (const auto& i : tmpAr1)
+ {
+ if (i.find(field) != std::string::npos)
+ {
+ std::vector<std::string> tmpAr2 = StringUtils::Split(i, "=");
+ if (tmpAr2.size() == 2)
+ {
+ StringUtils::Replace(tmpAr2[1], "\"", "");//remove quotes
+ return tmpAr2[1];
+ }
+ }
+ }
+ return "";
+}
+
+bool CAirPlayServer::CTCPClient::checkAuthorization(const std::string& authStr,
+ const std::string& method,
+ const std::string& uri)
+{
+ bool authValid = true;
+
+ std::string username;
+
+ if (authStr.empty())
+ return false;
+
+ //first get username - we allow all usernames for airplay (usually it is AirPlay)
+ username = getFieldFromString(authStr, "username");
+ if (username.empty())
+ {
+ authValid = false;
+ }
+
+ //second check realm
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "realm") != AUTH_REALM)
+ {
+ authValid = false;
+ }
+ }
+
+ //third check nonce
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "nonce") != m_authNonce)
+ {
+ authValid = false;
+ }
+ }
+
+ //forth check uri
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "uri") != uri)
+ {
+ authValid = false;
+ }
+ }
+
+ //last check response
+ if (authValid)
+ {
+ std::string realm = AUTH_REALM;
+ std::string ourResponse = calcResponse(username, ServerInstance->m_password, realm, method, uri, m_authNonce);
+ std::string theirResponse = getFieldFromString(authStr, "response");
+ if (!StringUtils::EqualsNoCase(theirResponse, ourResponse))
+ {
+ authValid = false;
+ CLog::Log(LOGDEBUG, "AirAuth: response mismatch - our: {} theirs: {}", ourResponse,
+ theirResponse);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "AirAuth: successful authentication from AirPlay client");
+ }
+ }
+ m_bAuthenticated = authValid;
+ return m_bAuthenticated;
+}
+
+void CAirPlayServer::backupVolume()
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ if (ServerInstance && ServerInstance->m_origVolume == -1)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ ServerInstance->m_origVolume = static_cast<int>(appVolume->GetVolumePercent());
+ }
+}
+
+void CAirPlayServer::restoreVolume()
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (ServerInstance && ServerInstance->m_origVolume != -1 &&
+ settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(static_cast<float>(ServerInstance->m_origVolume));
+ ServerInstance->m_origVolume = -1;
+ }
+}
+
+std::string getStringFromPlist(plist_t node)
+{
+ std::string ret;
+ char *tmpStr = nullptr;
+ plist_get_string_val(node, &tmpStr);
+ ret = tmpStr;
+ free(tmpStr);
+ return ret;
+}
+
+int CAirPlayServer::CTCPClient::ProcessRequest( std::string& responseHeader,
+ std::string& responseBody)
+{
+ std::string method = m_httpParser->getMethod() ? m_httpParser->getMethod() : "";
+ std::string uri = m_httpParser->getUri() ? m_httpParser->getUri() : "";
+ std::string queryString = m_httpParser->getQueryString() ? m_httpParser->getQueryString() : "";
+ std::string body = m_httpParser->getBody() ? m_httpParser->getBody() : "";
+ std::string contentType = m_httpParser->getValue("content-type") ? m_httpParser->getValue("content-type") : "";
+ m_sessionId = m_httpParser->getValue("x-apple-session-id") ? m_httpParser->getValue("x-apple-session-id") : "";
+ std::string authorization = m_httpParser->getValue("authorization") ? m_httpParser->getValue("authorization") : "";
+ std::string photoAction = m_httpParser->getValue("x-apple-assetaction") ? m_httpParser->getValue("x-apple-assetaction") : "";
+ std::string photoCacheId = m_httpParser->getValue("x-apple-assetkey") ? m_httpParser->getValue("x-apple-assetkey") : "";
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int status = AIRPLAY_STATUS_OK;
+ bool needAuth = false;
+
+ if (m_sessionId.empty())
+ m_sessionId = "00000000-0000-0000-0000-000000000000";
+
+ if (ServerInstance->m_usePassword && !m_bAuthenticated)
+ {
+ needAuth = true;
+ }
+
+ size_t startQs = uri.find('?');
+ if (startQs != std::string::npos)
+ {
+ uri.erase(startQs);
+ }
+
+ // This is the socket which will be used for reverse HTTP
+ // negotiate reverse HTTP via upgrade
+ if (uri == "/reverse")
+ {
+ status = AIRPLAY_STATUS_SWITCHING_PROTOCOLS;
+ responseHeader = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
+ }
+
+ // The rate command is used to play/pause media.
+ // A value argument should be supplied which indicates media should be played or paused.
+ // 0.000000 => pause
+ // 1.000000 => play
+ else if (uri == "/rate")
+ {
+ const char* found = strstr(queryString.c_str(), "value=");
+ int rate = found ? (int)(atof(found + strlen("value=")) + 0.5) : 0;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {} with rate {}", uri, rate);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (rate == 0)
+ {
+ if (appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ }
+ else
+ {
+ if (appPlayer->IsPausedPlayback())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ }
+ }
+
+ // The volume command is used to change playback volume.
+ // A value argument should be supplied which indicates how loud we should get.
+ // 0.000000 => silent
+ // 1.000000 => loud
+ else if (uri == "/volume")
+ {
+ const char* found = strstr(queryString.c_str(), "volume=");
+ float volume = found ? (float)strtod(found + strlen("volume="), NULL) : 0;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {} with volume {:f}", uri, volume);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (volume >= 0 && volume <= 1)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ float oldVolume = appVolume->GetVolumePercent();
+ volume *= 100;
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (oldVolume != volume &&
+ settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ backupVolume();
+ appVolume->SetVolume(volume);
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_VOLUME_SHOW, oldVolume < volume ? ACTION_VOLUME_UP : ACTION_VOLUME_DOWN);
+ }
+ }
+ }
+
+
+ // Contains a header like format in the request body which should contain a
+ // Content-Location and optionally a Start-Position
+ else if (uri == "/play")
+ {
+ std::string location;
+ float position = 0.0;
+ bool startPlayback = true;
+ m_lastEvent = EVENT_NONE;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (contentType == "application/x-apple-binary-plist")
+ {
+ CAirPlayServer::m_isPlaying++;
+
+ const char* bodyChr = m_httpParser->getBody();
+
+ plist_t dict = NULL;
+ plist_from_bin(bodyChr, m_httpParser->getContentLength(), &dict);
+
+ if (plist_dict_get_size(dict))
+ {
+ plist_t tmpNode = plist_dict_get_item(dict, "Start-Position");
+ if (tmpNode)
+ {
+ double tmpDouble = 0;
+ plist_get_real_val(tmpNode, &tmpDouble);
+ position = (float)tmpDouble;
+ }
+
+ tmpNode = plist_dict_get_item(dict, "Content-Location");
+ if (tmpNode)
+ {
+ location = getStringFromPlist(tmpNode);
+ tmpNode = NULL;
+ }
+
+ tmpNode = plist_dict_get_item(dict, "rate");
+ if (tmpNode)
+ {
+ double rate = 0;
+ plist_get_real_val(tmpNode, &rate);
+ if (rate == 0.0)
+ {
+ startPlayback = false;
+ }
+ tmpNode = NULL;
+ }
+
+ // in newer protocol versions the location is given
+ // via host and path where host is ip:port and path is /path/file.mov
+ if (location.empty())
+ tmpNode = plist_dict_get_item(dict, "host");
+ if (tmpNode)
+ {
+ location = "http://";
+ location += getStringFromPlist(tmpNode);
+
+ tmpNode = plist_dict_get_item(dict, "path");
+ if (tmpNode)
+ {
+ location += getStringFromPlist(tmpNode);
+ }
+ }
+
+ if (dict)
+ {
+ plist_free(dict);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Error parsing plist");
+ }
+ }
+ else
+ {
+ CAirPlayServer::m_isPlaying++;
+ // Get URL to play
+ std::string contentLocation = "Content-Location: ";
+ size_t start = body.find(contentLocation);
+ if (start == std::string::npos)
+ return AIRPLAY_STATUS_NOT_IMPLEMENTED;
+ start += contentLocation.size();
+ int end = body.find('\n', start);
+ location = body.substr(start, end - start);
+
+ std::string startPosition = "Start-Position: ";
+ start = body.find(startPosition);
+ if (start != std::string::npos)
+ {
+ start += startPosition.size();
+ int end = body.find('\n', start);
+ std::string positionStr = body.substr(start, end - start);
+ position = (float)atof(positionStr.c_str());
+ }
+ }
+
+ if (status != AIRPLAY_STATUS_NEED_AUTH)
+ {
+ std::string userAgent(CURL::Encode("AppleCoreMedia/1.0.0.8F455 (AppleTV; U; CPU OS 4_3 like Mac OS X; de_de)"));
+ location += "|User-Agent=" + userAgent;
+
+ CFileItem fileToPlay(location, false);
+ fileToPlay.SetProperty("StartPercent", position*100.0f);
+ ServerInstance->AnnounceToClients(EVENT_LOADING);
+
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(fileToPlay));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l));
+
+ // allow starting the player paused in ios8 mode (needed by camera roll app)
+ if (!startPlayback)
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ appPlayer->SeekPercentage(position * 100.0f);
+ }
+ }
+ }
+
+ // Used to perform seeking (POST request) and to retrieve current player position (GET request).
+ // GET scrub seems to also set rate 1 - strange but true
+ else if (uri == "/scrub")
+ {
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (method == "GET")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got GET request {}", uri);
+
+ if (appPlayer->GetTotalTime())
+ {
+ float position = static_cast<float>(appPlayer->GetTime()) / 1000;
+ responseBody =
+ StringUtils::Format("duration: {:.6f}\r\nposition: {:.6f}\r\n",
+ static_cast<float>(appPlayer->GetTotalTime()) / 1000, position);
+ }
+ else
+ {
+ status = AIRPLAY_STATUS_METHOD_NOT_ALLOWED;
+ }
+ }
+ else
+ {
+ const char* found = strstr(queryString.c_str(), "position=");
+
+ if (found && appPlayer->HasPlayer())
+ {
+ int64_t position = (int64_t) (atof(found + strlen("position=")) * 1000.0);
+ appPlayer->SeekTime(position);
+ CLog::Log(LOGDEBUG, "AIRPLAY: got POST request {} with pos {}", uri, position);
+ }
+ }
+ }
+
+ // Sent when media playback should be stopped
+ else if (uri == "/stop")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else
+ {
+ if (IsPlaying()) //only stop player if we started him
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ CAirPlayServer::m_isPlaying--;
+ }
+ else //if we are not playing and get the stop request - we just wanna stop picture streaming
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+ }
+ }
+ ClearPhotoAssetCache();
+ }
+
+ // RAW JPEG data is contained in the request body
+ else if (uri == "/photo")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (m_httpParser->getContentLength() > 0 || photoAction == "displayCached")
+ {
+ XFILE::CFile tmpFile;
+ std::string tmpFileName = "special://temp/airplayasset";
+ bool showPhoto = true;
+ bool receivePhoto = true;
+
+
+ if (photoAction == "cacheOnly")
+ showPhoto = false;
+ else if (photoAction == "displayCached")
+ {
+ receivePhoto = false;
+ if (photoCacheId.length())
+ CLog::Log(LOGDEBUG, "AIRPLAY: Trying to show from cache asset: {}", photoCacheId);
+ }
+
+ if (photoCacheId.length())
+ tmpFileName += photoCacheId;
+ else
+ tmpFileName += "airplay_photo";
+
+ if( receivePhoto && m_httpParser->getContentLength() > 3 &&
+ m_httpParser->getBody()[1] == 'P' &&
+ m_httpParser->getBody()[2] == 'N' &&
+ m_httpParser->getBody()[3] == 'G')
+ {
+ tmpFileName += ".png";
+ }
+ else
+ {
+ tmpFileName += ".jpg";
+ }
+
+ int writtenBytes=0;
+ if (receivePhoto)
+ {
+ if (tmpFile.OpenForWrite(tmpFileName, true))
+ {
+ writtenBytes = tmpFile.Write(m_httpParser->getBody(), m_httpParser->getContentLength());
+ tmpFile.Close();
+ }
+ if (photoCacheId.length())
+ CLog::Log(LOGDEBUG, "AIRPLAY: Cached asset: {}", photoCacheId);
+ }
+
+ if (showPhoto)
+ {
+ if ((writtenBytes > 0 && (unsigned int)writtenBytes == m_httpParser->getContentLength()) || !receivePhoto)
+ {
+ if (!receivePhoto && !XFILE::CFile::Exists(tmpFileName))
+ {
+ status = AIRPLAY_STATUS_PRECONDITION_FAILED; //image not found in the cache
+ if (photoCacheId.length())
+ CLog::Log(LOGWARNING, "AIRPLAY: Asset {} not found in our cache.", photoCacheId);
+ }
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PICTURE_SHOW, -1, -1, nullptr,
+ tmpFileName);
+ }
+ else
+ {
+ CLog::Log(LOGERROR,"AirPlayServer: Error writing tmpFile.");
+ }
+ }
+ }
+ }
+
+ else if (uri == "/playback-info")
+ {
+ float position = 0.0f;
+ float duration = 0.0f;
+ float cachePosition = 0.0f;
+ bool playing = false;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (appPlayer->HasPlayer())
+ {
+ if (appPlayer->GetTotalTime())
+ {
+ position = static_cast<float>(appPlayer->GetTime()) / 1000;
+ duration = static_cast<float>(appPlayer->GetTotalTime()) / 1000;
+ playing = !appPlayer->IsPaused();
+ cachePosition = position + (duration * appPlayer->GetCachePercentage() / 100.0f);
+ }
+
+ responseBody = StringUtils::Format(PLAYBACK_INFO, duration, cachePosition, position, (playing ? 1 : 0), duration);
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+
+ if (appPlayer->IsCaching())
+ {
+ CAirPlayServer::ServerInstance->AnnounceToClients(EVENT_LOADING);
+ }
+ }
+ else
+ {
+ responseBody = StringUtils::Format(PLAYBACK_INFO_NOT_READY);
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ }
+ }
+
+ else if (uri == "/server-info")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ responseBody = StringUtils::Format(
+ SERVER_INFO, CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetMacAddress());
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ }
+
+ else if (uri == "/slideshow-features")
+ {
+ // Ignore for now.
+ }
+
+ else if (uri == "/authorize")
+ {
+ // DRM, ignore for now.
+ }
+
+ else if (uri == "/setProperty")
+ {
+ status = AIRPLAY_STATUS_NOT_FOUND;
+ }
+
+ else if (uri == "/getProperty")
+ {
+ status = AIRPLAY_STATUS_NOT_FOUND;
+ }
+
+ else if (uri == "/fp-setup")
+ {
+ status = AIRPLAY_STATUS_PRECONDITION_FAILED;
+ }
+
+ else if (uri == "200") //response OK from the event reverse message
+ {
+ status = AIRPLAY_STATUS_NO_RESPONSE_NEEDED;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: unhandled request [{}]", uri);
+ status = AIRPLAY_STATUS_NOT_IMPLEMENTED;
+ }
+
+ if (status == AIRPLAY_STATUS_NEED_AUTH)
+ {
+ ComposeAuthRequestAnswer(responseHeader, responseBody);
+ }
+
+ return status;
+}
diff --git a/xbmc/network/AirPlayServer.h b/xbmc/network/AirPlayServer.h
new file mode 100644
index 0000000..986616b
--- /dev/null
+++ b/xbmc/network/AirPlayServer.h
@@ -0,0 +1,108 @@
+/*
+ * Many concepts and protocol specification in this code are taken from
+ * the Boxee project. http://www.boxee.tv
+ *
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/IAnnouncer.h"
+#include "network/Network.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/HttpParser.h"
+
+#include <map>
+#include <vector>
+
+#include <sys/socket.h>
+
+class CVariant;
+
+#define AIRPLAY_SERVER_VERSION_STR "101.28"
+
+class CAirPlayServer : public CThread, public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ // IAnnouncer IF
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ //AirPlayServer impl.
+ static bool StartServer(int port, bool nonlocal);
+ static void StopServer(bool bWait);
+ static bool IsRunning();
+ static bool SetCredentials(bool usePassword, const std::string& password);
+ static bool IsPlaying(){ return m_isPlaying > 0;}
+ static void backupVolume();
+ static void restoreVolume();
+ static int m_isPlaying;
+
+protected:
+ void Process() override;
+
+private:
+ CAirPlayServer(int port, bool nonlocal);
+ ~CAirPlayServer() override;
+ bool SetInternalCredentials(bool usePassword, const std::string& password);
+ bool Initialize();
+ void Deinitialize();
+ void AnnounceToClients(int state);
+
+ class CTCPClient
+ {
+ public:
+ CTCPClient();
+ ~CTCPClient();
+ //Copying a CCriticalSection is not allowed, so copy everything but that
+ //when adding a member variable, make sure to copy it in CTCPClient::Copy
+ CTCPClient(const CTCPClient& client);
+ CTCPClient& operator=(const CTCPClient& client);
+ void PushBuffer(CAirPlayServer *host, const char *buffer,
+ int length, std::string &sessionId,
+ std::map<std::string, int> &reverseSockets);
+ void ComposeReverseEvent(std::string& reverseHeader, std::string& reverseBody, int state);
+
+ void Disconnect();
+
+ int m_socket;
+ struct sockaddr_storage m_cliaddr;
+ socklen_t m_addrlen;
+ CCriticalSection m_critSection;
+ int m_sessionCounter;
+ std::string m_sessionId;
+
+ private:
+ int ProcessRequest( std::string& responseHeader,
+ std::string& response);
+
+ void ComposeAuthRequestAnswer(std::string& responseHeader, std::string& responseBody);
+ bool checkAuthorization(const std::string& authStr, const std::string& method, const std::string& uri);
+ void Copy(const CTCPClient& client);
+
+ HttpParser* m_httpParser;
+ bool m_bAuthenticated;
+ int m_lastEvent;
+ std::string m_authNonce;
+ };
+
+ CCriticalSection m_connectionLock;
+ std::vector<CTCPClient> m_connections;
+ std::map<std::string, int> m_reverseSockets;
+ std::vector<SOCKET> m_ServerSockets;
+ int m_port;
+ bool m_nonlocal;
+ bool m_usePassword;
+ std::string m_password;
+ int m_origVolume;
+
+ static CCriticalSection ServerInstanceLock;
+ static CAirPlayServer *ServerInstance;
+};
diff --git a/xbmc/network/AirTunesServer.cpp b/xbmc/network/AirTunesServer.cpp
new file mode 100644
index 0000000..0add358
--- /dev/null
+++ b/xbmc/network/AirTunesServer.cpp
@@ -0,0 +1,753 @@
+/*
+ * Many concepts and protocol specification in this code are taken
+ * from Shairport, by James Laird.
+ *
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "AirTunesServer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/ApplicationActionListeners.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h"
+#include "filesystem/File.h"
+#include "filesystem/PipeFile.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/tags/MusicInfoTag.h"
+#include "network/Network.h"
+#include "network/Zeroconf.h"
+#include "network/ZeroconfBrowser.h"
+#include "network/dacp/dacp.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/EndianSwap.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <cstring>
+#include <map>
+#include <mutex>
+#include <string>
+#include <utility>
+
+#if !defined(TARGET_WINDOWS)
+#pragma GCC diagnostic ignored "-Wwrite-strings"
+#endif
+
+#ifdef HAS_AIRPLAY
+#include "network/AirPlayServer.h"
+#endif
+
+#define TMP_COVERART_PATH_JPG "special://temp/airtunes_album_thumb.jpg"
+#define TMP_COVERART_PATH_PNG "special://temp/airtunes_album_thumb.png"
+#define ZEROCONF_DACP_SERVICE "_dacp._tcp"
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+CAirTunesServer *CAirTunesServer::ServerInstance = NULL;
+std::string CAirTunesServer::m_macAddress;
+std::string CAirTunesServer::m_metadata[3];
+CCriticalSection CAirTunesServer::m_metadataLock;
+bool CAirTunesServer::m_streamStarted = false;
+CCriticalSection CAirTunesServer::m_dacpLock;
+CDACP *CAirTunesServer::m_pDACP = NULL;
+std::string CAirTunesServer::m_dacp_id;
+std::string CAirTunesServer::m_active_remote_header;
+CCriticalSection CAirTunesServer::m_actionQueueLock;
+std::list<CAction> CAirTunesServer::m_actionQueue;
+CEvent CAirTunesServer::m_processActions;
+int CAirTunesServer::m_sampleRate = 44100;
+
+unsigned int CAirTunesServer::m_cachedStartTime = 0;
+unsigned int CAirTunesServer::m_cachedEndTime = 0;
+unsigned int CAirTunesServer::m_cachedCurrentTime = 0;
+
+
+//parse daap metadata - thx to project MythTV
+std::map<std::string, std::string> decodeDMAP(const char *buffer, unsigned int size)
+{
+ std::map<std::string, std::string> result;
+ unsigned int offset = 8;
+ while (offset < size)
+ {
+ std::string tag;
+ tag.append(buffer + offset, 4);
+ offset += 4;
+ uint32_t length = Endian_SwapBE32(*(const uint32_t *)(buffer + offset));
+ offset += sizeof(uint32_t);
+ std::string content;
+ content.append(buffer + offset, length);//possible fixme - utf8?
+ offset += length;
+ result[tag] = content;
+ }
+ return result;
+}
+
+void CAirTunesServer::ResetMetadata()
+{
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+
+ XFILE::CFile::Delete(TMP_COVERART_PATH_JPG);
+ XFILE::CFile::Delete(TMP_COVERART_PATH_PNG);
+ RefreshCoverArt();
+
+ m_metadata[0] = "";
+ m_metadata[1] = "AirPlay";
+ m_metadata[2] = "";
+ RefreshMetadata();
+}
+
+void CAirTunesServer::RefreshMetadata()
+{
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+ MUSIC_INFO::CMusicInfoTag tag;
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ if (infoMgr.GetCurrentSongTag())
+ tag = *infoMgr.GetCurrentSongTag();
+ if (m_metadata[0].length())
+ tag.SetAlbum(m_metadata[0]);//album
+ if (m_metadata[1].length())
+ tag.SetTitle(m_metadata[1]);//title
+ if (m_metadata[2].length())
+ tag.SetArtist(m_metadata[2]);//artist
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 1, -1,
+ static_cast<void*>(new CFileItem(tag)));
+}
+
+void CAirTunesServer::RefreshCoverArt(const char *outputFilename/* = NULL*/)
+{
+ static std::string coverArtFile = TMP_COVERART_PATH_JPG;
+
+ if (outputFilename != NULL)
+ coverArtFile = std::string(outputFilename);
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+ //reset to empty before setting the new one
+ //else it won't get refreshed because the name didn't change
+ infoMgr.SetCurrentAlbumThumb("");
+ //update the ui
+ infoMgr.SetCurrentAlbumThumb(coverArtFile);
+ //update the ui
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_REFRESH_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CAirTunesServer::SetMetadataFromBuffer(const char *buffer, unsigned int size)
+{
+
+ std::map<std::string, std::string> metadata = decodeDMAP(buffer, size);
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+
+ if(metadata["asal"].length())
+ m_metadata[0] = metadata["asal"];//album
+ if(metadata["minm"].length())
+ m_metadata[1] = metadata["minm"];//title
+ if(metadata["asar"].length())
+ m_metadata[2] = metadata["asar"];//artist
+
+ RefreshMetadata();
+}
+
+void CAirTunesServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if ((flag & ANNOUNCEMENT::Player) &&
+ sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ {
+ if ((message == "OnPlay" || message == "OnResume") && m_streamStarted)
+ {
+ RefreshMetadata();
+ RefreshCoverArt();
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ m_pDACP->Play();
+ }
+
+ if (message == "OnStop" && m_streamStarted)
+ {
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ m_pDACP->Stop();
+ }
+
+ if (message == "OnPause" && m_streamStarted)
+ {
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ m_pDACP->Pause();
+ }
+ }
+}
+
+void CAirTunesServer::EnableActionProcessing(bool enable)
+{
+ ServerInstance->RegisterActionListener(enable);
+}
+
+bool CAirTunesServer::OnAction(const CAction &action)
+{
+ switch(action.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ case ACTION_PREV_ITEM:
+ case ACTION_VOLUME_UP:
+ case ACTION_VOLUME_DOWN:
+ case ACTION_MUTE:
+ {
+ std::unique_lock<CCriticalSection> lock(m_actionQueueLock);
+ m_actionQueue.push_back(action);
+ m_processActions.Set();
+ }
+ }
+ return false;
+}
+
+void CAirTunesServer::Process()
+{
+ m_bStop = false;
+ while(!m_bStop)
+ {
+ if (m_streamStarted)
+ SetupRemoteControl();// check for remote controls
+
+ m_processActions.Wait(1000ms); // timeout for being able to stop
+ std::list<CAction> currentActions;
+ {
+ std::unique_lock<CCriticalSection> lock(m_actionQueueLock); // copy and clear the source queue
+ currentActions.insert(currentActions.begin(), m_actionQueue.begin(), m_actionQueue.end());
+ m_actionQueue.clear();
+ }
+
+ for (const auto& currentAction : currentActions)
+ {
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ {
+ switch(currentAction.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ m_pDACP->NextItem();
+ break;
+ case ACTION_PREV_ITEM:
+ m_pDACP->PrevItem();
+ break;
+ case ACTION_VOLUME_UP:
+ m_pDACP->VolumeUp();
+ break;
+ case ACTION_VOLUME_DOWN:
+ m_pDACP->VolumeDown();
+ break;
+ case ACTION_MUTE:
+ m_pDACP->ToggleMute();
+ break;
+ }
+ }
+ }
+ }
+}
+
+bool IsJPEG(const char *buffer, unsigned int size)
+{
+ bool ret = false;
+ if (size < 2)
+ return false;
+
+ //JPEG image files begin with FF D8 and end with FF D9.
+ // check for FF D8 big + little endian on start
+ if ((buffer[0] == (char)0xd8 && buffer[1] == (char)0xff) ||
+ (buffer[1] == (char)0xd8 && buffer[0] == (char)0xff))
+ ret = true;
+
+ if (ret)
+ {
+ ret = false;
+ //check on FF D9 big + little endian on end
+ if ((buffer[size - 2] == (char)0xd9 && buffer[size - 1] == (char)0xff) ||
+ (buffer[size - 1] == (char)0xd9 && buffer[size - 2] == (char)0xff))
+ ret = true;
+ }
+
+ return ret;
+}
+
+void CAirTunesServer::SetCoverArtFromBuffer(const char *buffer, unsigned int size)
+{
+ XFILE::CFile tmpFile;
+ std::string tmpFilename = TMP_COVERART_PATH_PNG;
+
+ if(!size)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+
+ if (IsJPEG(buffer, size))
+ tmpFilename = TMP_COVERART_PATH_JPG;
+
+ if (tmpFile.OpenForWrite(tmpFilename, true))
+ {
+ int writtenBytes=0;
+ writtenBytes = tmpFile.Write(buffer, size);
+ tmpFile.Close();
+
+ if (writtenBytes > 0)
+ RefreshCoverArt(tmpFilename.c_str());
+ }
+}
+
+void CAirTunesServer::FreeDACPRemote()
+{
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ delete m_pDACP;
+ m_pDACP = NULL;
+}
+
+#define RSA_KEY " \
+-----BEGIN RSA PRIVATE KEY-----\
+MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\
+wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\
+wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\
+/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\
+UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\
+BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\
+LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\
+NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\
+lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\
+aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\
+a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\
+oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\
+oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\
+k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\
+AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\
+cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\
+54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\
+17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\
+1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\
+LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\
+2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\
+-----END RSA PRIVATE KEY-----"
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_metadata(void *cls, void *session, const void *buffer, int buflen)
+{
+ CAirTunesServer::SetMetadataFromBuffer((const char *)buffer, buflen);
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_coverart(void *cls, void *session, const void *buffer, int buflen)
+{
+ CAirTunesServer::SetCoverArtFromBuffer((const char *)buffer, buflen);
+}
+
+char session[]="Kodi-AirTunes";
+
+void* CAirTunesServer::AudioOutputFunctions::audio_init(void *cls, int bits, int channels, int samplerate)
+{
+ XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls;
+ const CURL pathToUrl(XFILE::PipesManager::GetInstance().GetUniquePipeName());
+ pipe->OpenForWrite(pathToUrl);
+ pipe->SetOpenThreshold(300);
+
+ Demux_BXA_FmtHeader header;
+ std::memcpy(header.fourcc, "BXA ", 4);
+ header.type = BXA_PACKET_TYPE_FMT_DEMUX;
+ header.bitsPerSample = bits;
+ header.channels = channels;
+ header.sampleRate = samplerate;
+ header.durationMs = 0;
+
+ if (pipe->Write(&header, sizeof(header)) == 0)
+ return 0;
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+
+ CFileItem *item = new CFileItem();
+ item->SetPath(pipe->GetName());
+ item->SetMimeType("audio/x-xbmc-pcm");
+ m_streamStarted = true;
+ m_sampleRate = samplerate;
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(item));
+
+ // Not all airplay streams will provide metadata (e.g. if using mirroring,
+ // no metadata will be sent). If there *is* metadata, it will be received
+ // in a later call to audio_set_metadata/audio_set_coverart.
+ ResetMetadata();
+
+ // browse for dacp services protocol which gives us the remote control service
+ CZeroconfBrowser::GetInstance()->Start();
+ CZeroconfBrowser::GetInstance()->AddServiceType(ZEROCONF_DACP_SERVICE);
+ CAirTunesServer::EnableActionProcessing(true);
+
+ return session;//session
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_remote_control_id(void *cls, const char *dacp_id, const char *active_remote_header)
+{
+ if (dacp_id && active_remote_header)
+ {
+ m_dacp_id = dacp_id;
+ m_active_remote_header = active_remote_header;
+ }
+}
+
+void CAirTunesServer::InformPlayerAboutPlayTimes()
+{
+ if (m_cachedEndTime > 0)
+ {
+ unsigned int duration = m_cachedEndTime - m_cachedStartTime;
+ unsigned int position = m_cachedCurrentTime - m_cachedStartTime;
+ duration /= m_sampleRate;
+ position /= m_sampleRate;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ appPlayer->SetTime(position * 1000);
+ appPlayer->SetTotalTime(duration * 1000);
+
+ // reset play times now that we have informed the player
+ m_cachedEndTime = 0;
+ m_cachedCurrentTime = 0;
+ m_cachedStartTime = 0;
+
+ }
+ }
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_progress(void *cls, void *session, unsigned int start, unsigned int curr, unsigned int end)
+{
+ m_cachedStartTime = start;
+ m_cachedCurrentTime = curr;
+ m_cachedEndTime = end;
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ // player is there - directly inform him about play times
+ InformPlayerAboutPlayTimes();
+ }
+}
+
+void CAirTunesServer::SetupRemoteControl()
+{
+ // check if we found the remote control service via zeroconf already or
+ // if no valid id and headers was received yet
+ if (m_dacp_id.empty() || m_active_remote_header.empty() || m_pDACP != NULL)
+ return;
+
+ // check for the service matching m_dacp_id
+ std::vector<CZeroconfBrowser::ZeroconfService> services = CZeroconfBrowser::GetInstance()->GetFoundServices();
+ for (auto service : services )
+ {
+ if (StringUtils::EqualsNoCase(service.GetType(), std::string(ZEROCONF_DACP_SERVICE) + "."))
+ {
+#define DACP_NAME_PREFIX "iTunes_Ctrl_"
+ // name has the form "iTunes_Ctrl_56B29BB6CB904862"
+ // were we are interested in the 56B29BB6CB904862 identifier
+ if (StringUtils::StartsWithNoCase(service.GetName(), DACP_NAME_PREFIX))
+ {
+ std::vector<std::string> tokens = StringUtils::Split(service.GetName(), DACP_NAME_PREFIX);
+ // if we found the service matching the given identifier
+ if (tokens.size() > 1 && tokens[1] == m_dacp_id)
+ {
+ // resolve the service and save it
+ CZeroconfBrowser::GetInstance()->ResolveService(service);
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ // recheck with lock hold
+ if (m_pDACP == NULL)
+ {
+ // we can control the client with this object now
+ m_pDACP = new CDACP(m_active_remote_header, service.GetIP(), service.GetPort());
+ }
+ break;
+ }
+ }
+ }
+ }
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_volume(void *cls, void *session, float volume)
+{
+ //volume from -30 - 0 - -144 means mute
+ float volPercent = volume < -30.0f ? 0 : 1 - volume/-30;
+#ifdef HAS_AIRPLAY
+ CAirPlayServer::backupVolume();
+#endif
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(volPercent, false); //non-percent volume 0.0-1.0
+ }
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_process(void *cls, void *session, const void *buffer, int buflen)
+{
+ XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls;
+ pipe->Write(buffer, buflen);
+
+ // in case there are some play times cached that are not yet sent to the player - do it here
+ InformPlayerAboutPlayTimes();
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_destroy(void *cls, void *session)
+{
+ XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls;
+ pipe->SetEof();
+ pipe->Close();
+
+ CAirTunesServer::FreeDACPRemote();
+ m_dacp_id.clear();
+ m_active_remote_header.clear();
+
+ //fix airplay video for ios5 devices
+ //on ios5 when airplaying video
+ //the client first opens an airtunes stream
+ //while the movie is loading
+ //in that case we don't want to stop the player here
+ //because this would stop the airplaying video
+#ifdef HAS_AIRPLAY
+ if (!CAirPlayServer::IsPlaying())
+#endif
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ CLog::Log(LOGDEBUG, "AIRTUNES: AirPlay not running - stopping player");
+ }
+
+ m_streamStarted = false;
+
+ // no need to browse for dacp services while we don't receive
+ // any airtunes streams...
+ CZeroconfBrowser::GetInstance()->RemoveServiceType(ZEROCONF_DACP_SERVICE);
+ CZeroconfBrowser::GetInstance()->Stop();
+ CAirTunesServer::EnableActionProcessing(false);
+}
+
+void shairplay_log(void *cls, int level, const char *msg)
+{
+ int xbmcLevel = LOGINFO;
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGAIRTUNES))
+ return;
+
+ switch(level)
+ {
+ case RAOP_LOG_EMERG: // system is unusable
+ case RAOP_LOG_ALERT: // action must be taken immediately
+ case RAOP_LOG_CRIT: // critical conditions
+ xbmcLevel = LOGFATAL;
+ break;
+ case RAOP_LOG_ERR: // error conditions
+ xbmcLevel = LOGERROR;
+ break;
+ case RAOP_LOG_WARNING: // warning conditions
+ xbmcLevel = LOGWARNING;
+ break;
+ case RAOP_LOG_NOTICE: // normal but significant condition
+ case RAOP_LOG_INFO: // informational
+ xbmcLevel = LOGINFO;
+ break;
+ case RAOP_LOG_DEBUG: // debug-level messages
+ xbmcLevel = LOGDEBUG;
+ break;
+ default:
+ break;
+ }
+ CLog::Log(xbmcLevel, "AIRTUNES: {}", msg);
+}
+
+bool CAirTunesServer::StartServer(int port, bool nonlocal, bool usePassword, const std::string &password/*=""*/)
+{
+ bool success = false;
+ std::string pw = password;
+ CNetworkInterface *net = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ StopServer(true);
+
+ if (net)
+ {
+ m_macAddress = net->GetMacAddress();
+ StringUtils::Replace(m_macAddress, ":","");
+ while (m_macAddress.size() < 12)
+ {
+ m_macAddress = '0' + m_macAddress;
+ }
+ }
+ else
+ {
+ m_macAddress = "000102030405";
+ }
+
+ if (!usePassword)
+ {
+ pw.clear();
+ }
+
+ ServerInstance = new CAirTunesServer(port, nonlocal);
+ if (ServerInstance->Initialize(pw))
+ {
+ success = true;
+ std::string appName = StringUtils::Format("{}@{}", m_macAddress, CSysInfo::GetDeviceName());
+
+ std::vector<std::pair<std::string, std::string> > txt;
+ txt.emplace_back("txtvers", "1");
+ txt.emplace_back("cn", "0,1");
+ txt.emplace_back("ch", "2");
+ txt.emplace_back("ek", "1");
+ txt.emplace_back("et", "0,1");
+ txt.emplace_back("sv", "false");
+ txt.emplace_back("tp", "UDP");
+ txt.emplace_back("sm", "false");
+ txt.emplace_back("ss", "16");
+ txt.emplace_back("sr", "44100");
+ txt.emplace_back("pw", usePassword ? "true" : "false");
+ txt.emplace_back("vn", "3");
+ txt.emplace_back("da", "true");
+ txt.emplace_back("md", "0,1,2");
+ txt.emplace_back("am", "Kodi,1");
+ txt.emplace_back("vs", "130.14");
+
+ CZeroconf::GetInstance()->PublishService("servers.airtunes", "_raop._tcp", appName, port, txt);
+ }
+
+ return success;
+}
+
+void CAirTunesServer::StopServer(bool bWait)
+{
+ if (ServerInstance)
+ {
+ ServerInstance->Deinitialize();
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+
+ CZeroconf::GetInstance()->RemoveService("servers.airtunes");
+ }
+}
+
+bool CAirTunesServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return ServerInstance->IsRAOPRunningInternal();
+}
+
+bool CAirTunesServer::IsRAOPRunningInternal()
+{
+ if (m_pRaop)
+ {
+ return raop_is_running(m_pRaop) != 0;
+ }
+
+ return false;
+}
+
+CAirTunesServer::CAirTunesServer(int port, bool nonlocal)
+ : CThread("AirTunesActionThread")
+{
+ m_port = port;
+ m_pPipe = new XFILE::CPipeFile;
+}
+
+CAirTunesServer::~CAirTunesServer()
+{
+ delete m_pPipe;
+}
+
+void CAirTunesServer::RegisterActionListener(bool doRegister)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appListener = components.GetComponent<CApplicationActionListeners>();
+
+ if (doRegister)
+ {
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ appListener->RegisterActionListener(this);
+ ServerInstance->Create();
+ }
+ else
+ {
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ appListener->UnregisterActionListener(this);
+ ServerInstance->StopThread(true);
+ }
+}
+
+bool CAirTunesServer::Initialize(const std::string &password)
+{
+ bool ret = false;
+
+ Deinitialize();
+
+ raop_callbacks_t ao = {};
+ ao.cls = m_pPipe;
+ ao.audio_init = AudioOutputFunctions::audio_init;
+ ao.audio_set_volume = AudioOutputFunctions::audio_set_volume;
+ ao.audio_set_metadata = AudioOutputFunctions::audio_set_metadata;
+ ao.audio_set_coverart = AudioOutputFunctions::audio_set_coverart;
+ ao.audio_process = AudioOutputFunctions::audio_process;
+ ao.audio_destroy = AudioOutputFunctions::audio_destroy;
+ ao.audio_remote_control_id = AudioOutputFunctions::audio_remote_control_id;
+ ao.audio_set_progress = AudioOutputFunctions::audio_set_progress;
+ m_pRaop = raop_init(1, &ao, RSA_KEY, nullptr); //1 - we handle one client at a time max
+
+ if (m_pRaop)
+ {
+ char macAdr[6];
+ unsigned short port = (unsigned short)m_port;
+
+ raop_set_log_level(m_pRaop, RAOP_LOG_WARNING);
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGAIRTUNES))
+ {
+ raop_set_log_level(m_pRaop, RAOP_LOG_DEBUG);
+ }
+
+ raop_set_log_callback(m_pRaop, shairplay_log, NULL);
+
+ CNetworkInterface* net = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+
+ if (net)
+ {
+ net->GetMacAddressRaw(macAdr);
+ }
+
+ ret = raop_start(m_pRaop, &port, macAdr, 6, password.c_str()) >= 0;
+ }
+ return ret;
+}
+
+void CAirTunesServer::Deinitialize()
+{
+ RegisterActionListener(false);
+
+ if (m_pRaop)
+ {
+ raop_stop(m_pRaop);
+ raop_destroy(m_pRaop);
+ m_pRaop = nullptr;
+ }
+}
diff --git a/xbmc/network/AirTunesServer.h b/xbmc/network/AirTunesServer.h
new file mode 100644
index 0000000..a739ca7
--- /dev/null
+++ b/xbmc/network/AirTunesServer.h
@@ -0,0 +1,101 @@
+/*
+ * Many concepts and protocol specification in this code are taken from
+ * the Boxee project. http://www.boxee.tv
+ *
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "filesystem/PipeFile.h"
+#include "interfaces/IActionListener.h"
+#include "interfaces/IAnnouncer.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <list>
+#include <string>
+#include <vector>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <shairplay/raop.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+class CDACP;
+class CVariant;
+
+class CAirTunesServer : public ANNOUNCEMENT::IAnnouncer, public IActionListener, public CThread
+{
+public:
+ // ANNOUNCEMENT::IAnnouncer
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ void RegisterActionListener(bool doRegister);
+ static void EnableActionProcessing(bool enable);
+ // IACtionListener
+ bool OnAction(const CAction &action) override;
+
+ //CThread
+ void Process() override;
+
+ static bool StartServer(int port, bool nonlocal, bool usePassword, const std::string &password="");
+ static void StopServer(bool bWait);
+ static bool IsRunning();
+ bool IsRAOPRunningInternal();
+ static void SetMetadataFromBuffer(const char *buffer, unsigned int size);
+ static void SetCoverArtFromBuffer(const char *buffer, unsigned int size);
+ static void SetupRemoteControl();
+ static void FreeDACPRemote();
+
+private:
+ CAirTunesServer(int port, bool nonlocal);
+ ~CAirTunesServer() override;
+ bool Initialize(const std::string &password);
+ void Deinitialize();
+ static void RefreshCoverArt(const char *outputFilename = NULL);
+ static void RefreshMetadata();
+ static void ResetMetadata();
+ static void InformPlayerAboutPlayTimes();
+
+ int m_port;
+ raop_t* m_pRaop = nullptr;
+ XFILE::CPipeFile *m_pPipe;
+ static CAirTunesServer *ServerInstance;
+ static std::string m_macAddress;
+ static CCriticalSection m_metadataLock;
+ static std::string m_metadata[3];//0 - album, 1 - title, 2 - artist
+ static bool m_streamStarted;
+ static CCriticalSection m_dacpLock;
+ static CDACP *m_pDACP;
+ static std::string m_dacp_id;
+ static std::string m_active_remote_header;
+ static CCriticalSection m_actionQueueLock;
+ static std::list<CAction> m_actionQueue;
+ static CEvent m_processActions;
+ static int m_sampleRate;
+ static unsigned int m_cachedStartTime;
+ static unsigned int m_cachedEndTime;
+ static unsigned int m_cachedCurrentTime;
+
+ class AudioOutputFunctions
+ {
+ public:
+ static void* audio_init(void *cls, int bits, int channels, int samplerate);
+ static void audio_set_volume(void *cls, void *session, float volume);
+ static void audio_set_metadata(void *cls, void *session, const void *buffer, int buflen);
+ static void audio_set_coverart(void *cls, void *session, const void *buffer, int buflen);
+ static void audio_process(void *cls, void *session, const void *buffer, int buflen);
+ static void audio_destroy(void *cls, void *session);
+ static void audio_remote_control_id(void *cls, const char *identifier, const char *active_remote_header);
+ static void audio_set_progress(void *cls, void *session, unsigned int start, unsigned int curr, unsigned int end);
+ };
+};
diff --git a/xbmc/network/CMakeLists.txt b/xbmc/network/CMakeLists.txt
new file mode 100644
index 0000000..400e151
--- /dev/null
+++ b/xbmc/network/CMakeLists.txt
@@ -0,0 +1,60 @@
+set(SOURCES DNSNameCache.cpp
+ EventClient.cpp
+ EventPacket.cpp
+ EventServer.cpp
+ GUIDialogNetworkSetup.cpp
+ Network.cpp
+ NetworkServices.cpp
+ Socket.cpp
+ TCPServer.cpp
+ UdpClient.cpp
+ WakeOnAccess.cpp
+ ZeroconfBrowser.cpp
+ Zeroconf.cpp)
+
+set(HEADERS DNSNameCache.h
+ EventClient.h
+ EventPacket.h
+ EventServer.h
+ GUIDialogNetworkSetup.h
+ Network.h
+ NetworkServices.h
+ Socket.h
+ TCPServer.h
+ UdpClient.h
+ WakeOnAccess.h
+ Zeroconf.h
+ ZeroconfBrowser.h)
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES cddb.cpp)
+ list(APPEND HEADERS cddb.h)
+endif()
+
+if(PLIST_FOUND)
+ list(APPEND SOURCES AirPlayServer.cpp)
+ list(APPEND HEADERS AirPlayServer.h)
+endif()
+
+if(SHAIRPLAY_FOUND)
+ list(APPEND SOURCES AirTunesServer.cpp)
+ list(APPEND HEADERS AirTunesServer.h)
+endif()
+
+if(SMBCLIENT_FOUND)
+ list(APPEND HEADERS IWSDiscovery.h)
+endif()
+
+if(MICROHTTPD_FOUND)
+ list(APPEND SOURCES WebServer.cpp)
+ list(APPEND HEADERS WebServer.h)
+endif()
+
+core_add_library(network)
+if(BLUETOOTH_FOUND)
+ target_compile_definitions(${CORE_LIBRARY} PRIVATE -DHAVE_LIBBLUETOOTH=1)
+endif()
+
+if(ENABLE_STATIC_LIBS AND ENABLE_UPNP)
+ target_link_libraries(${CORE_LIBRARY} PRIVATE upnp)
+endif()
diff --git a/xbmc/network/DNSNameCache.cpp b/xbmc/network/DNSNameCache.cpp
new file mode 100644
index 0000000..af2ac1a
--- /dev/null
+++ b/xbmc/network/DNSNameCache.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "DNSNameCache.h"
+
+#include "threads/CriticalSection.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_FILESYSTEM_SMB)
+#include "ServiceBroker.h"
+
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+#endif
+
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <netinet/in.h>
+
+CDNSNameCache g_DNSCache;
+
+CCriticalSection CDNSNameCache::m_critical;
+
+CDNSNameCache::CDNSNameCache(void) = default;
+
+CDNSNameCache::~CDNSNameCache(void) = default;
+
+bool CDNSNameCache::Lookup(const std::string& strHostName, std::string& strIpAddress)
+{
+ if (strHostName.empty() && strIpAddress.empty())
+ return false;
+
+ // first see if this is already an ip address
+ unsigned long address = inet_addr(strHostName.c_str());
+ strIpAddress.clear();
+
+ if (address != INADDR_NONE)
+ {
+ strIpAddress = StringUtils::Format("{}.{}.{}.{}", (address & 0xFF), (address & 0xFF00) >> 8,
+ (address & 0xFF0000) >> 16, (address & 0xFF000000) >> 24);
+ return true;
+ }
+
+ // check if there's a custom entry or if it's already cached
+ if(g_DNSCache.GetCached(strHostName, strIpAddress))
+ return true;
+
+ // perform dns lookup
+ struct hostent *host = gethostbyname(strHostName.c_str());
+ if (host && host->h_addr_list[0])
+ {
+ strIpAddress = StringUtils::Format("{}.{}.{}.{}", (unsigned char)host->h_addr_list[0][0],
+ (unsigned char)host->h_addr_list[0][1],
+ (unsigned char)host->h_addr_list[0][2],
+ (unsigned char)host->h_addr_list[0][3]);
+ g_DNSCache.Add(strHostName, strIpAddress);
+ return true;
+ }
+
+ CLog::Log(LOGERROR, "Unable to lookup host: '{}'", strHostName);
+ return false;
+}
+
+bool CDNSNameCache::GetCached(const std::string& strHostName, std::string& strIpAddress)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ // loop through all DNSname entries and see if strHostName is cached
+ for (const auto& DNSname : g_DNSCache.m_vecDNSNames)
+ {
+ if (DNSname.m_strHostName == strHostName)
+ {
+ strIpAddress = DNSname.m_strIpAddress;
+ return true;
+ }
+ }
+ }
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_FILESYSTEM_SMB)
+ if (WSDiscovery::CWSDiscoveryPosix::IsInitialized())
+ {
+ WSDiscovery::CWSDiscoveryPosix& WSInstance =
+ dynamic_cast<WSDiscovery::CWSDiscoveryPosix&>(CServiceBroker::GetWSDiscovery());
+ if (WSInstance.GetCached(strHostName, strIpAddress))
+ return true;
+ }
+ else
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CDNSNameCache::GetCached: CWSDiscoveryPosix not initialized");
+#endif
+
+ // not cached
+ return false;
+}
+
+void CDNSNameCache::Add(const std::string& strHostName, const std::string& strIpAddress)
+{
+ CDNSName dnsName;
+
+ dnsName.m_strHostName = strHostName;
+ dnsName.m_strIpAddress = strIpAddress;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ g_DNSCache.m_vecDNSNames.push_back(dnsName);
+}
+
diff --git a/xbmc/network/DNSNameCache.h b/xbmc/network/DNSNameCache.h
new file mode 100644
index 0000000..25c2943
--- /dev/null
+++ b/xbmc/network/DNSNameCache.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+class CCriticalSection;
+
+class CDNSNameCache
+{
+public:
+ class CDNSName
+ {
+ public:
+ std::string m_strHostName;
+ std::string m_strIpAddress;
+ };
+ CDNSNameCache(void);
+ virtual ~CDNSNameCache(void);
+ static void Add(const std::string& strHostName, const std::string& strIpAddress);
+ static bool GetCached(const std::string& strHostName, std::string& strIpAddress);
+ static bool Lookup(const std::string& strHostName, std::string& strIpAddress);
+
+protected:
+ static CCriticalSection m_critical;
+ std::vector<CDNSName> m_vecDNSNames;
+};
diff --git a/xbmc/network/EventClient.cpp b/xbmc/network/EventClient.cpp
new file mode 100644
index 0000000..d3b67dc
--- /dev/null
+++ b/xbmc/network/EventClient.cpp
@@ -0,0 +1,788 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EventClient.h"
+
+#include "EventPacket.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/ButtonTranslator.h"
+#include "input/GamepadTranslator.h"
+#include "input/IRTranslator.h"
+#include "input/Key.h"
+#include "input/KeyboardTranslator.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <map>
+#include <mutex>
+#include <queue>
+
+using namespace EVENTCLIENT;
+using namespace EVENTPACKET;
+
+struct ButtonStateFinder
+{
+ explicit ButtonStateFinder(const CEventButtonState& state)
+ : m_keycode(state.m_iKeyCode)
+ , m_map(state.m_mapName)
+ , m_button(state.m_buttonName)
+ {}
+
+ bool operator()(const CEventButtonState& state)
+ {
+ return state.m_mapName == m_map
+ && state.m_iKeyCode == m_keycode
+ && state.m_buttonName == m_button;
+ }
+ private:
+ unsigned short m_keycode;
+ std::string m_map;
+ std::string m_button;
+};
+
+/************************************************************************/
+/* CEventButtonState */
+/************************************************************************/
+void CEventButtonState::Load()
+{
+ if ( m_iKeyCode == 0 )
+ {
+ if ( (m_mapName.length() > 0) && (m_buttonName.length() > 0) )
+ {
+ m_iKeyCode = CButtonTranslator::TranslateString(m_mapName, m_buttonName);
+ if (m_iKeyCode == 0)
+ {
+ Reset();
+ CLog::Log(LOGERROR, "ES: Could not map {} : {} to a key", m_mapName, m_buttonName);
+ }
+ }
+ }
+ else
+ {
+ if (m_mapName.length() > 3 &&
+ (StringUtils::StartsWith(m_mapName, "JS")) )
+ {
+ m_joystickName = m_mapName.substr(2); // <num>:joyname
+ m_iControllerNumber = (unsigned char)(*(m_joystickName.c_str()))
+ - (unsigned char)'0'; // convert <num> to int
+ m_joystickName = m_joystickName.substr(2); // extract joyname
+ }
+
+ if (m_mapName.length() > 3 &&
+ (StringUtils::StartsWith(m_mapName, "CC")) ) // custom map - CC:<controllerName>
+ {
+ m_customControllerName = m_mapName.substr(3);
+ }
+ }
+}
+
+/************************************************************************/
+/* CEventClient */
+/************************************************************************/
+bool CEventClient::AddPacket(std::unique_ptr<CEventPacket> packet)
+{
+ if (!packet)
+ return false;
+
+ ResetTimeout();
+ if ( packet->Size() > 1 )
+ {
+ //! @todo limit payload size
+ if (m_seqPackets[packet->Sequence()])
+ {
+ if(!m_bSequenceError)
+ CLog::Log(LOGWARNING,
+ "CEventClient::AddPacket - received packet with same sequence number ({}) as "
+ "previous packet from eventclient {}",
+ packet->Sequence(), m_deviceName);
+ m_bSequenceError = true;
+ m_seqPackets.erase(packet->Sequence());
+ }
+
+ unsigned int sequence = packet->Sequence();
+
+ m_seqPackets[sequence] = std::move(packet);
+ if (m_seqPackets.size() == m_seqPackets[sequence]->Size())
+ {
+ unsigned int iSeqPayloadSize = 0;
+ for (unsigned int i = 1; i <= m_seqPackets[sequence]->Size(); i++)
+ {
+ iSeqPayloadSize += m_seqPackets[i]->PayloadSize();
+ }
+
+ std::vector<uint8_t> newPayload(iSeqPayloadSize);
+ auto newPayloadIter = newPayload.begin();
+
+ unsigned int packets = m_seqPackets[sequence]->Size(); // packet can be deleted in this loop
+ for (unsigned int i = 1; i <= packets; i++)
+ {
+ newPayloadIter =
+ std::copy(m_seqPackets[i]->Payload(),
+ m_seqPackets[i]->Payload() + m_seqPackets[i]->PayloadSize(), newPayloadIter);
+
+ if (i > 1)
+ m_seqPackets.erase(i);
+ }
+ m_seqPackets[1]->SetPayload(newPayload);
+ m_readyPackets.push(std::move(m_seqPackets[1]));
+ m_seqPackets.clear();
+ }
+ }
+ else
+ {
+ m_readyPackets.push(std::move(packet));
+ }
+ return true;
+}
+
+void CEventClient::ProcessEvents()
+{
+ if (!m_readyPackets.empty())
+ {
+ while ( ! m_readyPackets.empty() )
+ {
+ ProcessPacket(m_readyPackets.front().get());
+ if ( ! m_readyPackets.empty() ) // in case the BYE packet cleared the queues
+ m_readyPackets.pop();
+ }
+ }
+}
+
+bool CEventClient::GetNextAction(CEventAction &action)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_actionQueue.empty())
+ {
+ // grab the next action in line
+ action = m_actionQueue.front();
+ m_actionQueue.pop();
+ return true;
+ }
+ else
+ {
+ // we got nothing
+ return false;
+ }
+}
+
+bool CEventClient::ProcessPacket(CEventPacket *packet)
+{
+ if (!packet)
+ return false;
+
+ bool valid = false;
+
+ switch (packet->Type())
+ {
+ case PT_HELO:
+ valid = OnPacketHELO(packet);
+ break;
+
+ case PT_BYE:
+ valid = OnPacketBYE(packet);
+ break;
+
+ case PT_BUTTON:
+ valid = OnPacketBUTTON(packet);
+ break;
+
+ case EVENTPACKET::PT_MOUSE:
+ valid = OnPacketMOUSE(packet);
+ break;
+
+ case PT_NOTIFICATION:
+ valid = OnPacketNOTIFICATION(packet);
+ break;
+
+ case PT_PING:
+ valid = true;
+ break;
+
+ case PT_LOG:
+ valid = OnPacketLOG(packet);
+ break;
+
+ case PT_ACTION:
+ valid = OnPacketACTION(packet);
+ break;
+
+ default:
+ CLog::Log(LOGDEBUG, "ES: Got Unknown Packet");
+ break;
+ }
+
+ if (valid)
+ ResetTimeout();
+
+ return valid;
+}
+
+bool CEventClient::OnPacketHELO(CEventPacket *packet)
+{
+ //! @todo check it last HELO packet was received less than 5 minutes back
+ //! if so, do not show notification of connection.
+ if (Greeted())
+ return false;
+
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+
+ // parse device name
+ if (!ParseString(payload, psize, m_deviceName))
+ return false;
+
+ CLog::Log(LOGINFO, "ES: Incoming connection from {}", m_deviceName);
+
+ // icon type
+ unsigned char ltype;
+ if (!ParseByte(payload, psize, ltype))
+ return false;
+ m_eLogoType = (LogoType)ltype;
+
+ // client's port (if any)
+ unsigned short dport;
+ if (!ParseUInt16(payload, psize, dport))
+ return false;
+ m_iRemotePort = (unsigned int)dport;
+
+ // 2 x reserved uint32 (8 bytes)
+ unsigned int reserved;
+ ParseUInt32(payload, psize, reserved);
+ ParseUInt32(payload, psize, reserved);
+
+ // image data if any
+ std::string iconfile = "special://temp/helo";
+ if (m_eLogoType != LT_NONE && psize>0)
+ {
+ switch (m_eLogoType)
+ {
+ case LT_JPEG:
+ iconfile += ".jpg";
+ break;
+
+ case LT_GIF:
+ iconfile += ".gif";
+ break;
+
+ default:
+ iconfile += ".png";
+ break;
+ }
+ XFILE::CFile file;
+ if (!file.OpenForWrite(iconfile, true) || file.Write((const void *)payload, psize) != psize)
+ {
+ CLog::Log(LOGERROR, "ES: Could not write icon file");
+ m_eLogoType = LT_NONE;
+ }
+ }
+
+ m_bGreeted = true;
+ if (m_eLogoType == LT_NONE)
+ {
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(33200), m_deviceName);
+ }
+ else
+ {
+ CGUIDialogKaiToast::QueueNotification(iconfile, g_localizeStrings.Get(33200), m_deviceName);
+ }
+ return true;
+}
+
+bool CEventClient::OnPacketBYE(CEventPacket *packet)
+{
+ if (!Greeted())
+ return false;
+
+ m_bGreeted = false;
+ FreePacketQueues();
+ m_currentButton.Reset();
+
+ return true;
+}
+
+bool CEventClient::OnPacketBUTTON(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+
+ std::string map, button;
+ unsigned short flags;
+ unsigned short bcode;
+ unsigned short amount;
+
+ // parse the button code
+ if (!ParseUInt16(payload, psize, bcode))
+ return false;
+
+ // parse flags
+ if (!ParseUInt16(payload, psize, flags))
+ return false;
+
+ // parse amount
+ if (!ParseUInt16(payload, psize, amount))
+ return false;
+
+ // parse the map to use
+ if (!ParseString(payload, psize, map))
+ return false;
+
+ // parse button name
+ if (flags & PTB_USE_NAME)
+ {
+ if (!ParseString(payload, psize, button))
+ return false;
+ }
+
+ unsigned int keycode;
+ if(flags & PTB_USE_NAME)
+ keycode = 0;
+ else if(flags & PTB_VKEY)
+ keycode = bcode|KEY_VKEY;
+ else if(flags & PTB_UNICODE)
+ keycode = bcode|ES_FLAG_UNICODE;
+ else
+ keycode = bcode;
+
+ float famount = 0;
+ bool active = (flags & PTB_DOWN) ? true : false;
+
+ if (flags & PTB_USE_NAME)
+ CLog::Log(LOGDEBUG, "EventClient: button name \"{}\" map \"{}\" {}", button, map,
+ active ? "pressed" : "released");
+ else
+ CLog::Log(LOGDEBUG, "EventClient: button code {} {}", bcode, active ? "pressed" : "released");
+
+ if(flags & PTB_USE_AMOUNT)
+ {
+ if(flags & PTB_AXIS)
+ famount = (float)amount/65535.0f*2.0f-1.0f;
+ else
+ famount = (float)amount/65535.0f;
+ }
+ else
+ famount = (active ? 1.0f : 0.0f);
+
+ if(flags & PTB_QUEUE)
+ {
+ /* find the last queued item of this type */
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CEventButtonState state( keycode,
+ map,
+ button,
+ famount,
+ (flags & (PTB_AXIS|PTB_AXISSINGLE)) ? true : false,
+ (flags & PTB_NO_REPEAT) ? false : true,
+ (flags & PTB_USE_AMOUNT) ? true : false );
+
+ /* correct non active events so they work with rest of code */
+ if(!active)
+ {
+ state.m_bActive = false;
+ state.m_bRepeat = false;
+ state.m_fAmount = 0.0;
+ }
+
+ std::list<CEventButtonState>::reverse_iterator it;
+ it = find_if( m_buttonQueue.rbegin() , m_buttonQueue.rend(), ButtonStateFinder(state));
+
+ if(it == m_buttonQueue.rend())
+ {
+ if(active)
+ m_buttonQueue.push_back(state);
+ }
+ else
+ {
+ if(!active && it->m_bActive)
+ {
+ /* since modifying the list invalidates the reverse iterator */
+ std::list<CEventButtonState>::iterator it2 = (++it).base();
+
+ /* if last event had an amount, we must resend without amount */
+ if (it2->m_bUseAmount && it2->m_fAmount != 0.0f)
+ {
+ m_buttonQueue.push_back(state);
+ }
+
+ /* if the last event was waiting for a repeat interval, it has executed already.*/
+ if(it2->m_bRepeat)
+ {
+ if (it2->m_iNextRepeat.time_since_epoch().count() > 0)
+ {
+ m_buttonQueue.erase(it2);
+ }
+ else
+ {
+ it2->m_bRepeat = false;
+ it2->m_bActive = false;
+ }
+ }
+
+ }
+ else if(active && !it->m_bActive)
+ {
+ m_buttonQueue.push_back(state);
+ if (!state.m_bRepeat && state.m_bAxis && state.m_fAmount != 0.0f)
+ {
+ state.m_bActive = false;
+ state.m_bRepeat = false;
+ state.m_fAmount = 0.0;
+ m_buttonQueue.push_back(state);
+ }
+ }
+ else
+ it->m_fAmount = state.m_fAmount;
+ }
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if ( flags & PTB_DOWN )
+ {
+ m_currentButton.m_iKeyCode = keycode;
+ m_currentButton.m_mapName = map;
+ m_currentButton.m_buttonName = button;
+ m_currentButton.m_fAmount = famount;
+ m_currentButton.m_bRepeat = (flags & PTB_NO_REPEAT) ? false : true;
+ m_currentButton.m_bAxis = (flags & PTB_AXIS) ? true : false;
+ m_currentButton.m_iNextRepeat = {};
+ m_currentButton.SetActive();
+ m_currentButton.Load();
+ }
+ else
+ {
+ /* when a button is released that had amount, make sure *
+ * to resend the keypress with an amount of 0 */
+ if ((flags & PTB_USE_AMOUNT) && m_currentButton.m_fAmount > 0.0f)
+ {
+ CEventButtonState state( m_currentButton.m_iKeyCode,
+ m_currentButton.m_mapName,
+ m_currentButton.m_buttonName,
+ 0.0,
+ m_currentButton.m_bAxis,
+ false,
+ true );
+
+ m_buttonQueue.push_back (state);
+ }
+ m_currentButton.Reset();
+ }
+ }
+
+ return true;
+}
+
+bool CEventClient::OnPacketMOUSE(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ unsigned char flags;
+ unsigned short mx, my;
+
+ // parse flags
+ if (!ParseByte(payload, psize, flags))
+ return false;
+
+ // parse x position
+ if (!ParseUInt16(payload, psize, mx))
+ return false;
+
+ // parse x position
+ if (!ParseUInt16(payload, psize, my))
+ return false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if ( flags & PTM_ABSOLUTE )
+ {
+ m_iMouseX = mx;
+ m_iMouseY = my;
+ m_bMouseMoved = true;
+ }
+ }
+
+ return true;
+}
+
+bool CEventClient::OnPacketNOTIFICATION(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ std::string title, message;
+
+ // parse caption
+ if (!ParseString(payload, psize, title))
+ return false;
+
+ // parse message
+ if (!ParseString(payload, psize, message))
+ return false;
+
+ // icon type
+ unsigned char ltype;
+ if (!ParseByte(payload, psize, ltype))
+ return false;
+ m_eLogoType = (LogoType)ltype;
+
+ // reserved uint32
+ unsigned int reserved;
+ ParseUInt32(payload, psize, reserved);
+
+ // image data if any
+ std::string iconfile = "special://temp/notification";
+ if (m_eLogoType != LT_NONE && psize>0)
+ {
+ switch (m_eLogoType)
+ {
+ case LT_JPEG:
+ iconfile += ".jpg";
+ break;
+
+ case LT_GIF:
+ iconfile += ".gif";
+ break;
+
+ default:
+ iconfile += ".png";
+ break;
+ }
+
+ XFILE::CFile file;
+ if (!file.OpenForWrite(iconfile, true) || file.Write((const void *)payload, psize) != psize)
+ {
+ CLog::Log(LOGERROR, "ES: Could not write icon file");
+ m_eLogoType = LT_NONE;
+ }
+ }
+
+ if (m_eLogoType == LT_NONE)
+ {
+ CGUIDialogKaiToast::QueueNotification(title, message);
+ }
+ else
+ {
+ CGUIDialogKaiToast::QueueNotification(iconfile, title, message);
+ }
+ return true;
+}
+
+bool CEventClient::OnPacketLOG(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ std::string logmsg;
+ unsigned char ltype;
+
+ if (!ParseByte(payload, psize, ltype))
+ return false;
+ if (!ParseString(payload, psize, logmsg))
+ return false;
+
+ CLog::Log((int)ltype, "{}", logmsg);
+ return true;
+}
+
+bool CEventClient::OnPacketACTION(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ std::string actionString;
+ unsigned char actionType;
+
+ if (!ParseByte(payload, psize, actionType))
+ return false;
+ if (!ParseString(payload, psize, actionString))
+ return false;
+
+ switch(actionType)
+ {
+ case AT_EXEC_BUILTIN:
+ case AT_BUTTON:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_actionQueue.push(CEventAction(actionString.c_str(), actionType));
+ }
+ break;
+
+ default:
+ CLog::Log(LOGDEBUG, "ES: Failed - ActionType: {} ActionString: {}", actionType, actionString);
+ return false;
+ break;
+ }
+ return true;
+}
+
+bool CEventClient::ParseString(unsigned char* &payload, int &psize, std::string& parsedVal)
+{
+ if (psize <= 0)
+ return false;
+
+ unsigned char *pos = (unsigned char *)memchr((void*)payload, (int)'\0', psize);
+ if (!pos)
+ return false;
+
+ parsedVal = (char*)payload;
+ psize -= ((pos - payload) + 1);
+ payload = pos+1;
+ return true;
+}
+
+bool CEventClient::ParseByte(unsigned char* &payload, int &psize, unsigned char& parsedVal)
+{
+ if (psize <= 0)
+ return false;
+
+ parsedVal = *payload;
+ payload++;
+ psize--;
+ return true;
+}
+
+bool CEventClient::ParseUInt32(unsigned char* &payload, int &psize, unsigned int& parsedVal)
+{
+ if (psize < 4)
+ return false;
+
+ parsedVal = ntohl(*((unsigned int *)payload));
+ payload+=4;
+ psize-=4;
+ return true;
+}
+
+bool CEventClient::ParseUInt16(unsigned char* &payload, int &psize, unsigned short& parsedVal)
+{
+ if (psize < 2)
+ return false;
+
+ parsedVal = ntohs(*((unsigned short *)payload));
+ payload+=2;
+ psize-=2;
+ return true;
+}
+
+void CEventClient::FreePacketQueues()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ while ( ! m_readyPackets.empty() )
+ m_readyPackets.pop();
+
+ m_seqPackets.clear();
+}
+
+unsigned int CEventClient::GetButtonCode(std::string& strMapName, bool& isAxis, float& amount, bool &isJoystick)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ unsigned int bcode = 0;
+
+ if ( m_currentButton.Active() )
+ {
+ bcode = m_currentButton.KeyCode();
+ strMapName = m_currentButton.JoystickName();
+ isJoystick = true;
+ if (strMapName.length() == 0)
+ {
+ strMapName = m_currentButton.CustomControllerName();
+ isJoystick = false;
+ }
+
+ isAxis = m_currentButton.Axis();
+ amount = m_currentButton.Amount();
+
+ if ( ! m_currentButton.Repeat() )
+ m_currentButton.Reset();
+ else
+ {
+ if ( ! CheckButtonRepeat(m_currentButton.m_iNextRepeat) )
+ bcode = 0;
+ }
+ return bcode;
+ }
+
+ if(m_buttonQueue.empty())
+ return 0;
+
+
+ std::list<CEventButtonState> repeat;
+ std::list<CEventButtonState>::iterator it;
+ for(it = m_buttonQueue.begin(); bcode == 0 && it != m_buttonQueue.end(); ++it)
+ {
+ bcode = it->KeyCode();
+ strMapName = it->JoystickName();
+ isJoystick = true;
+
+ if (strMapName.length() == 0)
+ {
+ strMapName = it->CustomControllerName();
+ isJoystick = false;
+ }
+
+ isAxis = it->Axis();
+ amount = it->Amount();
+
+ if(it->Repeat())
+ {
+ /* MUST update m_iNextRepeat before resend */
+ bool skip = !it->Axis() && !CheckButtonRepeat(it->m_iNextRepeat);
+
+ repeat.push_back(*it);
+ if(skip)
+ {
+ bcode = 0;
+ continue;
+ }
+ }
+ }
+
+ m_buttonQueue.erase(m_buttonQueue.begin(), it);
+ m_buttonQueue.insert(m_buttonQueue.end(), repeat.begin(), repeat.end());
+ return bcode;
+}
+
+bool CEventClient::GetMousePos(float& x, float& y)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bMouseMoved)
+ {
+ x = (m_iMouseX / 65535.0f) * CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ y = (m_iMouseY / 65535.0f) * CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+ m_bMouseMoved = false;
+ return true;
+ }
+ return false;
+}
+
+bool CEventClient::CheckButtonRepeat(std::chrono::time_point<std::chrono::steady_clock>& next)
+{
+ auto now = std::chrono::steady_clock::now();
+
+ if (next.time_since_epoch().count() == 0)
+ {
+ next = now + m_iRepeatDelay;
+ return true;
+ }
+ else if ( now > next )
+ {
+ next = now + m_iRepeatSpeed;
+ return true;
+ }
+ return false;
+}
+
+bool CEventClient::Alive() const
+{
+ // 60 seconds timeout
+ if ( (time(NULL) - m_lastPing) > 60 )
+ return false;
+ return true;
+}
diff --git a/xbmc/network/EventClient.h b/xbmc/network/EventClient.h
new file mode 100644
index 0000000..b5fe5e1
--- /dev/null
+++ b/xbmc/network/EventClient.h
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "EventPacket.h"
+#include "ServiceBroker.h"
+#include "Socket.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <chrono>
+#include <list>
+#include <map>
+#include <queue>
+#include <utility>
+
+namespace EVENTCLIENT
+{
+
+ #define ES_FLAG_UNICODE 0x80000000 // new 16bit key flag to support real unicode over EventServer
+
+ class CEventAction
+ {
+ public:
+ CEventAction()
+ {
+ actionType = 0;
+ }
+ CEventAction(const char* action, unsigned char type):
+ actionName(action)
+ {
+ actionType = type;
+ }
+
+ std::string actionName;
+ unsigned char actionType;
+ };
+
+ class CEventButtonState
+ {
+ public:
+ CEventButtonState()
+ {
+ m_iKeyCode = 0;
+ m_fAmount = 0.0f;
+ m_bUseAmount = false;
+ m_bRepeat = false;
+ m_bActive = false;
+ m_bAxis = false;
+ m_iControllerNumber = 0;
+ m_iNextRepeat = {};
+ }
+
+ CEventButtonState(unsigned int iKeyCode,
+ std::string mapName,
+ std::string buttonName,
+ float fAmount,
+ bool isAxis,
+ bool bRepeat,
+ bool bUseAmount)
+ : m_buttonName(std::move(buttonName)), m_mapName(std::move(mapName))
+ {
+ m_iKeyCode = iKeyCode;
+ m_fAmount = fAmount;
+ m_bUseAmount = bUseAmount;
+ m_bRepeat = bRepeat;
+ m_bActive = true;
+ m_bAxis = isAxis;
+ m_iControllerNumber = 0;
+ m_iNextRepeat = {};
+ Load();
+ }
+
+ void Reset() { m_bActive = false; }
+ void SetActive() { m_bActive = true; }
+ bool Active() const { return m_bActive; }
+ bool Repeat() const { return m_bRepeat; }
+ int ControllerNumber() const { return m_iControllerNumber; }
+ bool Axis() const { return m_bAxis; }
+ unsigned int KeyCode() const { return m_iKeyCode; }
+ float Amount() const { return m_fAmount; }
+ void Load();
+ const std::string& JoystickName() const { return m_joystickName; }
+ const std::string& CustomControllerName() const { return m_customControllerName; }
+
+ // data
+ unsigned int m_iKeyCode;
+ unsigned short m_iControllerNumber;
+ std::string m_buttonName;
+ std::string m_mapName;
+ std::string m_joystickName;
+ std::string m_customControllerName;
+ float m_fAmount;
+ bool m_bUseAmount;
+ bool m_bRepeat;
+ bool m_bActive;
+ bool m_bAxis;
+ std::chrono::time_point<std::chrono::steady_clock> m_iNextRepeat;
+ };
+
+
+ /**********************************************************************/
+ /* UDP EventClient Class */
+ /**********************************************************************/
+ // - clients timeout if they don't receive at least 1 ping in 1 minute
+ // - sequence packets timeout after 5 seconds
+ class CEventClient
+ {
+ public:
+ CEventClient()
+ {
+ Initialize();
+ }
+
+ explicit CEventClient(SOCKETS::CAddress& addr):
+ m_remoteAddr(addr)
+ {
+ Initialize();
+ }
+
+ void Initialize()
+ {
+ m_bGreeted = false;
+ m_iMouseX = 0;
+ m_iMouseY = 0;
+ m_iCurrentSeqLen = 0;
+ m_lastPing = 0;
+ m_lastSeq = 0;
+ m_iRemotePort = 0;
+ m_bMouseMoved = false;
+ m_bSequenceError = false;
+ RefreshSettings();
+ }
+
+ const std::string& Name() const
+ {
+ return m_deviceName;
+ }
+
+ void RefreshSettings()
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_iRepeatDelay =
+ std::chrono::milliseconds(settings->GetInt(CSettings::SETTING_SERVICES_ESINITIALDELAY));
+ m_iRepeatSpeed = std::chrono::milliseconds(
+ settings->GetInt(CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY));
+ }
+
+ SOCKETS::CAddress& Address()
+ {
+ return m_remoteAddr;
+ }
+
+ virtual ~CEventClient()
+ {
+ FreePacketQueues();
+ }
+
+ // add packet to queue
+ bool AddPacket(std::unique_ptr<EVENTPACKET::CEventPacket> packet);
+
+ // return true if client received ping with the last 1 minute
+ bool Alive() const;
+
+ // process the packet queue
+ bool ProcessQueue();
+
+ // process the queued up events (packets)
+ void ProcessEvents();
+
+ // gets the next action in the action queue
+ bool GetNextAction(CEventAction& action);
+
+ // deallocate all packets in the queues
+ void FreePacketQueues();
+
+ // return event states
+ unsigned int GetButtonCode(std::string& strMapName, bool& isAxis, float& amount, bool &isJoystick);
+
+ // update mouse position
+ bool GetMousePos(float& x, float& y);
+
+ protected:
+ bool ProcessPacket(EVENTPACKET::CEventPacket *packet);
+
+ // packet handlers
+ virtual bool OnPacketHELO(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketBYE(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketBUTTON(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketMOUSE(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketNOTIFICATION(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketLOG(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketACTION(EVENTPACKET::CEventPacket *packet);
+ bool CheckButtonRepeat(std::chrono::time_point<std::chrono::steady_clock>& next);
+
+ // returns true if the client has received the HELO packet
+ bool Greeted() { return m_bGreeted; }
+
+ // reset the timeout counter
+ void ResetTimeout()
+ {
+ m_lastPing = time(NULL);
+ }
+
+ // helper functions
+
+ // Parses a null terminated string from payload.
+ // After parsing successfully:
+ // 1. payload is incremented to end of string
+ // 2. psize is decremented by length of string
+ // 3. parsedVal contains the parsed string
+ // 4. true is returned
+ bool ParseString(unsigned char* &payload, int &psize, std::string& parsedVal);
+
+ // Parses a single byte (same behavior as ParseString)
+ bool ParseByte(unsigned char* &payload, int &psize, unsigned char& parsedVal);
+
+ // Parse a single 32-bit integer (converts from network order to host order)
+ bool ParseUInt32(unsigned char* &payload, int &psize, unsigned int& parsedVal);
+
+ // Parse a single 16-bit integer (converts from network order to host order)
+ bool ParseUInt16(unsigned char* &payload, int &psize, unsigned short& parsedVal);
+
+ std::string m_deviceName;
+ int m_iCurrentSeqLen;
+ time_t m_lastPing;
+ time_t m_lastSeq;
+ int m_iRemotePort;
+ bool m_bGreeted;
+ std::chrono::milliseconds m_iRepeatDelay;
+ std::chrono::milliseconds m_iRepeatSpeed;
+ unsigned int m_iMouseX;
+ unsigned int m_iMouseY;
+ bool m_bMouseMoved;
+ bool m_bSequenceError;
+
+ SOCKETS::CAddress m_remoteAddr;
+
+ EVENTPACKET::LogoType m_eLogoType;
+ CCriticalSection m_critSection;
+
+ std::map<unsigned int, std::unique_ptr<EVENTPACKET::CEventPacket>> m_seqPackets;
+ std::queue<std::unique_ptr<EVENTPACKET::CEventPacket>> m_readyPackets;
+
+ // button and mouse state
+ std::list<CEventButtonState> m_buttonQueue;
+ std::queue<CEventAction> m_actionQueue;
+ CEventButtonState m_currentButton;
+ };
+
+}
+
diff --git a/xbmc/network/EventPacket.cpp b/xbmc/network/EventPacket.cpp
new file mode 100644
index 0000000..17389f1
--- /dev/null
+++ b/xbmc/network/EventPacket.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EventPacket.h"
+
+#include "Socket.h"
+#include "utils/log.h"
+
+using namespace EVENTPACKET;
+
+/************************************************************************/
+/* CEventPacket */
+/************************************************************************/
+bool CEventPacket::Parse(int datasize, const void *data)
+{
+ unsigned char* buf = const_cast<unsigned char*>((const unsigned char *)data);
+ if (datasize < HEADER_SIZE || datasize > PACKET_SIZE)
+ return false;
+
+ // check signature
+ if (memcmp(data, (const void*)HEADER_SIG, HEADER_SIG_LENGTH) != 0)
+ return false;
+
+ buf += HEADER_SIG_LENGTH;
+
+ // extract protocol version
+ m_cMajVer = (*buf++);
+ m_cMinVer = (*buf++);
+
+ if (m_cMajVer != 2 && m_cMinVer != 0)
+ return false;
+
+ // get packet type
+ m_eType = (PacketType)ntohs(*((uint16_t*)buf));
+
+ if (m_eType < (unsigned short)PT_HELO || m_eType >= (unsigned short)PT_LAST)
+ return false;
+
+ // get packet sequence id
+ buf += 2;
+ m_iSeq = ntohl(*((uint32_t*)buf));
+
+ // get total message length
+ buf += 4;
+ m_iTotalPackets = ntohl(*((uint32_t*)buf));
+
+ // get payload size
+ buf += 4;
+ uint16_t payloadSize = ntohs(*(reinterpret_cast<uint16_t*>(buf)));
+
+ if ((payloadSize + HEADER_SIZE) != static_cast<uint16_t>(datasize))
+ return false;
+
+ // get the client's token
+ buf += 2;
+ m_iClientToken = ntohl(*((uint32_t*)buf));
+
+ buf += 4;
+
+ // get payload
+ if (payloadSize > 0)
+ {
+ // forward past reserved bytes
+ buf += 10;
+
+ m_pPayload = std::vector<uint8_t>(buf, buf + payloadSize);
+ }
+ m_bValid = true;
+ return true;
+}
+
+void CEventPacket::SetPayload(std::vector<uint8_t> payload)
+{
+ m_pPayload = std::move(payload);
+}
diff --git a/xbmc/network/EventPacket.h b/xbmc/network/EventPacket.h
new file mode 100644
index 0000000..40074e8
--- /dev/null
+++ b/xbmc/network/EventPacket.h
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <stdlib.h>
+#include <vector>
+
+namespace EVENTPACKET
+{
+ const int PACKET_SIZE = 1024;
+ const int HEADER_SIZE = 32;
+ const char HEADER_SIG[] = "XBMC";
+ const int HEADER_SIG_LENGTH = 4;
+
+ /************************************************************************/
+ /* */
+ /* - Generic packet structure (maximum 1024 bytes per packet) */
+ /* - Header is 32 bytes long, so 992 bytes available for payload */
+ /* - large payloads can be split into multiple packets using H4 and H5 */
+ /* H5 should contain total no. of packets in such a case */
+ /* - H6 contains length of P1, which is limited to 992 bytes */
+ /* - if H5 is 0 or 1, then H4 will be ignored (single packet msg) */
+ /* - H7 must be set to zeros for now */
+ /* */
+ /* ----------------------------- */
+ /* | -H1 Signature ("XBMC") | - 4 x CHAR 4B */
+ /* | -H2 Version (eg. 2.0) | - 2 x UNSIGNED CHAR 2B */
+ /* | -H3 PacketType | - 1 x UNSIGNED SHORT 2B */
+ /* | -H4 Sequence number | - 1 x UNSIGNED LONG 4B */
+ /* | -H5 No. of packets in msg | - 1 x UNSIGNED LONG 4B */
+ /* | -H6 Payload size | - 1 x UNSIGNED SHORT 2B */
+ /* | -H7 Client's unique token | - 1 x UNSIGNED LONG 4B */
+ /* | -H8 Reserved | - 10 x UNSIGNED CHAR 10B */
+ /* |---------------------------| */
+ /* | -P1 payload | - */
+ /* ----------------------------- */
+ /************************************************************************/
+
+ /************************************************************************
+ The payload format for each packet type is described below each
+ packet type.
+
+ Legend:
+ %s - null terminated ASCII string (strlen + '\0' bytes)
+ (empty string is represented as a single byte NULL '\0')
+ %c - single byte
+ %i - network byte ordered short unsigned integer (2 bytes)
+ %d - network byte ordered long unsigned integer (4 bytes)
+ XX - binary data prefixed with %d size
+ (can span multiple packets with <raw>)
+ raw - raw binary data
+ ************************************************************************/
+
+ enum LogoType
+ {
+ LT_NONE = 0x00,
+ LT_JPEG = 0x01,
+ LT_PNG = 0x02,
+ LT_GIF = 0x03
+ };
+
+ enum ButtonFlags
+ {
+ PTB_USE_NAME = 0x01,
+ PTB_DOWN = 0x02,
+ PTB_UP = 0x04,
+ PTB_USE_AMOUNT = 0x08,
+ PTB_QUEUE = 0x10,
+ PTB_NO_REPEAT = 0x20,
+ PTB_VKEY = 0x40,
+ PTB_AXIS = 0x80,
+ PTB_AXISSINGLE = 0x100,
+ PTB_UNICODE = 0x200
+ };
+
+ enum MouseFlags
+ {
+ PTM_ABSOLUTE = 0x01
+ /* PTM_RELATIVE = 0x02 */
+ };
+
+ enum ActionType
+ {
+ AT_EXEC_BUILTIN = 0x01,
+ AT_BUTTON = 0x02
+ };
+
+ enum PacketType
+ {
+ PT_HELO = 0x01,
+ /************************************************************************/
+ /* Payload format */
+ /* %s - device name (max 128 chars) */
+ /* %c - icontype ( 0=>NOICON, 1=>JPEG , 2=>PNG , 3=>GIF ) */
+ /* %s - my port ( 0=>not listening ) */
+ /* %d - reserved1 ( 0 ) */
+ /* %d - reserved2 ( 0 ) */
+ /* XX - imagedata ( can span multiple packets ) */
+ /************************************************************************/
+
+ PT_BYE = 0x02,
+ /************************************************************************/
+ /* no payload */
+ /************************************************************************/
+
+ PT_BUTTON = 0x03,
+ /************************************************************************/
+ /* Payload format */
+ /* %i - button code */
+ /* %i - flags 0x01 => use button map/name instead of code */
+ /* 0x02 => btn down */
+ /* 0x04 => btn up */
+ /* 0x08 => use amount */
+ /* 0x10 => queue event */
+ /* 0x20 => do not repeat */
+ /* 0x40 => virtual key */
+ /* 0x80 => axis key */
+ /* %i - amount ( 0 => 65k maps to -1 => 1 ) */
+ /* %s - device map (case sensitive and required if flags & 0x01) */
+ /* "KB" - Standard keyboard map */
+ /* "XG" - Xbox Gamepad */
+ /* "R1" - Xbox Remote */
+ /* "R2" - Xbox Universal Remote */
+ /* "LI:devicename" - valid LIRC device map where 'devicename' */
+ /* is the actual name of the LIRC device */
+ /* "JS<num>:joyname" - valid Joystick device map where */
+ /* 'joyname' is the name specified in */
+ /* the keymap. JS only supports button code */
+ /* and not button name currently (!0x01). */
+ /* %s - button name (required if flags & 0x01) */
+ /************************************************************************/
+
+ PT_MOUSE = 0x04,
+ /************************************************************************/
+ /* Payload format */
+ /* %c - flags */
+ /* - 0x01 absolute position */
+ /* %i - mousex (0-65535 => maps to screen width) */
+ /* %i - mousey (0-65535 => maps to screen height) */
+ /************************************************************************/
+
+ PT_PING = 0x05,
+ /************************************************************************/
+ /* no payload */
+ /************************************************************************/
+
+ PT_BROADCAST = 0x06,
+ /************************************************************************/
+ /* @todo implement */
+ /* Payload format: TODO */
+ /************************************************************************/
+
+ PT_NOTIFICATION = 0x07,
+ /************************************************************************/
+ /* Payload format: */
+ /* %s - caption */
+ /* %s - message */
+ /* %c - icontype ( 0=>NOICON, 1=>JPEG , 2=>PNG , 3=>GIF ) */
+ /* %d - reserved ( 0 ) */
+ /* XX - imagedata ( can span multiple packets ) */
+ /************************************************************************/
+
+ PT_BLOB = 0x08,
+ /************************************************************************/
+ /* Payload format */
+ /* raw - raw binary data */
+ /************************************************************************/
+
+ PT_LOG = 0x09,
+ /************************************************************************/
+ /* Payload format */
+ /* %c - log type */
+ /* %s - message */
+ /************************************************************************/
+ PT_ACTION = 0x0A,
+ /************************************************************************/
+ /* Payload format */
+ /* %c - action type */
+ /* %s - action message */
+ /************************************************************************/
+ PT_DEBUG = 0xFF,
+ /************************************************************************/
+ /* Payload format: */
+ /************************************************************************/
+
+ PT_LAST // NO-OP
+ };
+
+ class CEventPacket
+ {
+ public:
+ CEventPacket() = default;
+
+ explicit CEventPacket(int datasize, const void* data) { Parse(datasize, data); }
+
+ virtual ~CEventPacket() = default;
+ virtual bool Parse(int datasize, const void *data);
+ bool IsValid() const { return m_bValid; }
+ PacketType Type() const { return m_eType; }
+ unsigned int Size() const { return m_iTotalPackets; }
+ unsigned int Sequence() const { return m_iSeq; }
+ const uint8_t* Payload() const { return m_pPayload.data(); }
+ unsigned int PayloadSize() const { return m_pPayload.size(); }
+ unsigned int ClientToken() const { return m_iClientToken; }
+ void SetPayload(std::vector<uint8_t> payload);
+
+ protected:
+ bool m_bValid{false};
+ unsigned int m_iSeq{0};
+ unsigned int m_iTotalPackets{0};
+ unsigned char m_header[32];
+ std::vector<uint8_t> m_pPayload;
+ unsigned int m_iClientToken{0};
+ unsigned char m_cMajVer{'0'};
+ unsigned char m_cMinVer{'0'};
+ PacketType m_eType{PT_LAST};
+ };
+
+}
+
diff --git a/xbmc/network/EventServer.cpp b/xbmc/network/EventServer.cpp
new file mode 100644
index 0000000..3e25b59
--- /dev/null
+++ b/xbmc/network/EventServer.cpp
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EventServer.h"
+
+#include "EventClient.h"
+#include "EventPacket.h"
+#include "ServiceBroker.h"
+#include "Socket.h"
+#include "Zeroconf.h"
+#include "application/Application.h"
+#include "guilib/GUIAudioManager.h"
+#include "input/Key.h"
+#include "input/actions/ActionTranslator.h"
+#include "interfaces/builtins/Builtins.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+
+#include <cassert>
+#include <map>
+#include <mutex>
+#include <queue>
+
+using namespace EVENTSERVER;
+using namespace EVENTPACKET;
+using namespace EVENTCLIENT;
+using namespace SOCKETS;
+using namespace std::chrono_literals;
+
+/************************************************************************/
+/* CEventServer */
+/************************************************************************/
+std::unique_ptr<CEventServer> CEventServer::m_pInstance;
+
+CEventServer::CEventServer() : CThread("EventServer")
+{
+ m_bStop = false;
+ m_bRefreshSettings = false;
+
+ // default timeout in ms for receiving a single packet
+ m_iListenTimeout = 1000;
+}
+
+void CEventServer::RemoveInstance()
+{
+ m_pInstance.reset();
+}
+
+CEventServer* CEventServer::GetInstance()
+{
+ if (!m_pInstance)
+ m_pInstance = std::make_unique<CEventServer>();
+
+ return m_pInstance.get();
+}
+
+void CEventServer::StartServer()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bRunning)
+ return;
+
+ // set default port
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_iPort = settings->GetInt(CSettings::SETTING_SERVICES_ESPORT);
+ assert(m_iPort <= 65535 && m_iPort >= 1);
+
+ // max clients
+ m_iMaxClients = settings->GetInt(CSettings::SETTING_SERVICES_ESMAXCLIENTS);
+ if (m_iMaxClients < 0)
+ {
+ CLog::Log(LOGERROR, "ES: Invalid maximum number of clients specified {}", m_iMaxClients);
+ m_iMaxClients = 20;
+ }
+
+ CThread::Create();
+}
+
+void CEventServer::StopServer(bool bWait)
+{
+ CZeroconf::GetInstance()->RemoveService("services.eventserver");
+ StopThread(bWait);
+}
+
+void CEventServer::Cleanup()
+{
+ if (m_pSocket)
+ m_pSocket->Close();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_clients.clear();
+}
+
+int CEventServer::GetNumberOfClients()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_clients.size();
+}
+
+void CEventServer::Process()
+{
+ while(!m_bStop)
+ {
+ Run();
+ if (!m_bStop)
+ CThread::Sleep(1000ms);
+ }
+}
+
+void CEventServer::Run()
+{
+ CSocketListener listener;
+ int packetSize = 0;
+
+ CLog::Log(LOGINFO, "ES: Starting UDP Event server on port {}", m_iPort);
+
+ Cleanup();
+
+ // create socket and initialize buffer
+ m_pSocket = CSocketFactory::CreateUDPSocket();
+ if (!m_pSocket)
+ {
+ CLog::Log(LOGERROR, "ES: Could not create socket, aborting!");
+ return;
+ }
+
+ m_pPacketBuffer.resize(PACKET_SIZE);
+
+ // bind to IP and start listening on port
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ int port_range = settings->GetInt(CSettings::SETTING_SERVICES_ESPORTRANGE);
+ if (port_range < 1 || port_range > 100)
+ {
+ CLog::Log(LOGERROR, "ES: Invalid port range specified {}, defaulting to 10", port_range);
+ port_range = 10;
+ }
+ if (!m_pSocket->Bind(!settings->GetBool(CSettings::SETTING_SERVICES_ESALLINTERFACES), m_iPort, port_range))
+ {
+ CLog::Log(LOGERROR, "ES: Could not listen on port {}", m_iPort);
+ return;
+ }
+
+ // publish service
+ std::vector<std::pair<std::string, std::string> > txt;
+ CZeroconf::GetInstance()->PublishService("servers.eventserver",
+ "_xbmc-events._udp",
+ CSysInfo::GetDeviceName(),
+ m_iPort,
+ txt);
+
+ // add our socket to the 'select' listener
+ listener.AddSocket(m_pSocket.get());
+
+ m_bRunning = true;
+
+ while (!m_bStop)
+ {
+ try
+ {
+ // start listening until we timeout
+ if (listener.Listen(m_iListenTimeout))
+ {
+ CAddress addr;
+ if ((packetSize = m_pSocket->Read(addr, PACKET_SIZE, m_pPacketBuffer.data())) > -1)
+ {
+ ProcessPacket(addr, packetSize);
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "ES: Exception caught while listening for socket");
+ break;
+ }
+
+ // process events and queue the necessary actions and button codes
+ ProcessEvents();
+
+ // refresh client list
+ RefreshClients();
+
+ // broadcast
+ // BroadcastBeacon();
+ }
+
+ CLog::Log(LOGINFO, "ES: UDP Event server stopped");
+ m_bRunning = false;
+ Cleanup();
+}
+
+void CEventServer::ProcessPacket(CAddress& addr, int pSize)
+{
+ // check packet validity
+ std::unique_ptr<CEventPacket> packet =
+ std::make_unique<CEventPacket>(pSize, m_pPacketBuffer.data());
+ if (!packet)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory, cannot accept packet");
+ return;
+ }
+
+ unsigned int clientToken;
+
+ if (!packet->IsValid())
+ {
+ CLog::Log(LOGDEBUG, "ES: Received invalid packet");
+ return;
+ }
+
+ clientToken = packet->ClientToken();
+ if (!clientToken)
+ clientToken = addr.ULong(); // use IP if packet doesn't have a token
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // first check if we have a client for this address
+ auto iter = m_clients.find(clientToken);
+
+ if ( iter == m_clients.end() )
+ {
+ if ( m_clients.size() >= (unsigned int)m_iMaxClients)
+ {
+ CLog::Log(LOGWARNING, "ES: Cannot accept any more clients, maximum client count reached");
+ return;
+ }
+
+ // new client
+ auto client = std::make_unique<CEventClient>(addr);
+ if (!client)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory, cannot accept new client connection");
+ return;
+ }
+
+ m_clients[clientToken] = std::move(client);
+ }
+ m_clients[clientToken]->AddPacket(std::move(packet));
+}
+
+void CEventServer::RefreshClients()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+
+ while ( iter != m_clients.end() )
+ {
+ if (! (iter->second->Alive()))
+ {
+ CLog::Log(LOGINFO, "ES: Client {} from {} timed out", iter->second->Name(),
+ iter->second->Address().Address());
+ m_clients.erase(iter);
+ iter = m_clients.begin();
+ }
+ else
+ {
+ if (m_bRefreshSettings)
+ {
+ iter->second->RefreshSettings();
+ }
+ ++iter;
+ }
+ }
+ m_bRefreshSettings = false;
+}
+
+void CEventServer::ProcessEvents()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+
+ while (iter != m_clients.end())
+ {
+ iter->second->ProcessEvents();
+ ++iter;
+ }
+}
+
+bool CEventServer::ExecuteNextAction()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CEventAction actionEvent;
+ auto iter = m_clients.begin();
+
+ while (iter != m_clients.end())
+ {
+ if (iter->second->GetNextAction(actionEvent))
+ {
+ // Leave critical section before processing action
+ lock.unlock();
+ switch(actionEvent.actionType)
+ {
+ case AT_EXEC_BUILTIN:
+ CBuiltins::GetInstance().Execute(actionEvent.actionName);
+ break;
+
+ case AT_BUTTON:
+ {
+ unsigned int actionID;
+ CActionTranslator::TranslateString(actionEvent.actionName, actionID);
+ CAction action(actionID, 1.0f, 0.0f, actionEvent.actionName);
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().PlayActionSound(action);
+
+ g_application.OnAction(action);
+ }
+ break;
+ }
+ return true;
+ }
+ ++iter;
+ }
+
+ return false;
+}
+
+unsigned int CEventServer::GetButtonCode(std::string& strMapName, bool& isAxis, float& fAmount, bool &isJoystick)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+ unsigned int bcode = 0;
+
+ while (iter != m_clients.end())
+ {
+ bcode = iter->second->GetButtonCode(strMapName, isAxis, fAmount, isJoystick);
+ if (bcode)
+ return bcode;
+ ++iter;
+ }
+ return bcode;
+}
+
+bool CEventServer::GetMousePos(float &x, float &y)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+
+ while (iter != m_clients.end())
+ {
+ if (iter->second->GetMousePos(x, y))
+ return true;
+ ++iter;
+ }
+ return false;
+}
diff --git a/xbmc/network/EventServer.h b/xbmc/network/EventServer.h
new file mode 100644
index 0000000..8e529af
--- /dev/null
+++ b/xbmc/network/EventServer.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "EventClient.h"
+#include "Socket.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <vector>
+
+namespace EVENTSERVER
+{
+
+ /**********************************************************************/
+ /* UDP Event Server Class */
+ /**********************************************************************/
+ class CEventServer : private CThread
+ {
+ public:
+ static void RemoveInstance();
+ static CEventServer* GetInstance();
+
+ CEventServer();
+ ~CEventServer() override = default;
+
+ // IRunnable entry point for thread
+ void Process() override;
+
+ bool Running()
+ {
+ return m_bRunning;
+ }
+
+ void RefreshSettings()
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bRefreshSettings = true;
+ }
+
+ // start / stop server
+ void StartServer();
+ void StopServer(bool bWait);
+
+ // get events
+ unsigned int GetButtonCode(std::string& strMapName, bool& isAxis, float& amount, bool &isJoystick);
+ bool ExecuteNextAction();
+ bool GetMousePos(float &x, float &y);
+ int GetNumberOfClients();
+
+ protected:
+ void Cleanup();
+ void Run();
+ void ProcessPacket(SOCKETS::CAddress& addr, int packetSize);
+ void ProcessEvents();
+ void RefreshClients();
+
+ std::map<unsigned long, std::unique_ptr<EVENTCLIENT::CEventClient>> m_clients;
+ static std::unique_ptr<CEventServer> m_pInstance;
+ std::unique_ptr<SOCKETS::CUDPSocket> m_pSocket;
+ int m_iPort;
+ int m_iListenTimeout;
+ int m_iMaxClients;
+ std::vector<uint8_t> m_pPacketBuffer;
+ std::atomic<bool> m_bRunning = false;
+ CCriticalSection m_critSection;
+ bool m_bRefreshSettings;
+ };
+
+}
+
diff --git a/xbmc/network/GUIDialogNetworkSetup.cpp b/xbmc/network/GUIDialogNetworkSetup.cpp
new file mode 100644
index 0000000..e2fd165
--- /dev/null
+++ b/xbmc/network/GUIDialogNetworkSetup.cpp
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIDialogNetworkSetup.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/VFSEntry.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/lib/Setting.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+
+#define CONTROL_OK 28
+#define CONTROL_CANCEL 29
+
+#define SETTING_PROTOCOL "protocol"
+#define SETTING_SERVER_ADDRESS "serveraddress"
+#define SETTING_SERVER_BROWSE "serverbrowse"
+#define SETTING_PORT_NUMBER "portnumber"
+#define SETTING_USERNAME "username"
+#define SETTING_PASSWORD "password"
+#define SETTING_REMOTE_PATH "remotepath"
+
+CGUIDialogNetworkSetup::CGUIDialogNetworkSetup(void)
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_NETWORK_SETUP, "DialogSettings.xml")
+{
+ m_protocol = 0;
+ m_confirmed = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogNetworkSetup::~CGUIDialogNetworkSetup() = default;
+
+bool CGUIDialogNetworkSetup::OnBack(int actionID)
+{
+ m_confirmed = false;
+ return CGUIDialogSettingsManualBase::OnBack(actionID);
+}
+
+bool CGUIDialogNetworkSetup::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_OK)
+ {
+ OnOK();
+ return true;
+ }
+ else if (iControl == CONTROL_CANCEL)
+ {
+ OnCancel();
+ return true;
+ }
+ }
+ break;
+ }
+ return CGUIDialogSettingsManualBase::OnMessage(message);
+}
+
+void CGUIDialogNetworkSetup::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == SETTING_PROTOCOL)
+ {
+ m_server.clear();
+ m_path.clear();
+ m_username.clear();
+ m_password.clear();
+ OnProtocolChange();
+ }
+ else if (settingId == SETTING_SERVER_ADDRESS)
+ m_server = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_REMOTE_PATH)
+ m_path = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_PORT_NUMBER)
+ m_port = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_USERNAME)
+ m_username = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_PASSWORD)
+ m_password = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+}
+
+void CGUIDialogNetworkSetup::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == SETTING_SERVER_BROWSE)
+ OnServerBrowse();
+}
+
+// \brief Show CGUIDialogNetworkSetup dialog and prompt for a new network address.
+// \return True if the network address is valid, false otherwise.
+bool CGUIDialogNetworkSetup::ShowAndGetNetworkAddress(std::string &path)
+{
+ CGUIDialogNetworkSetup *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNetworkSetup>(WINDOW_DIALOG_NETWORK_SETUP);
+ if (!dialog) return false;
+ dialog->Initialize();
+ if (!dialog->SetPath(path))
+ {
+ HELPERS::ShowOKDialogText(CVariant{ 10218 }, CVariant{ 39103 });
+ return false;
+ }
+
+ dialog->Open();
+ path = dialog->ConstructPath();
+ return dialog->IsConfirmed();
+}
+
+void CGUIDialogNetworkSetup::OnInitWindow()
+{
+ // start as unconfirmed
+ m_confirmed = false;
+
+ CGUIDialogSettingsManualBase::OnInitWindow();
+
+ UpdateButtons();
+}
+
+void CGUIDialogNetworkSetup::OnDeinitWindow(int nextWindowID)
+{
+ // clear protocol spinner
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_PROTOCOL);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), settingControl->GetID());
+ OnMessage(msg);
+ }
+
+ CGUIDialogSettingsManualBase::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogNetworkSetup::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(1007);
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+}
+
+void CGUIDialogNetworkSetup::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("networksetupsettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogNetworkSetup: unable to setup settings");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogNetworkSetup: unable to setup settings");
+ return;
+ }
+
+ // Add our protocols
+ TranslatableIntegerSettingOptions labels;
+ for (size_t idx = 0; idx < m_protocols.size(); ++idx)
+ labels.push_back(
+ TranslatableIntegerSettingOption(m_protocols[idx].label, idx, m_protocols[idx].addonId));
+
+ AddSpinner(group, SETTING_PROTOCOL, 1008, SettingLevel::Basic, m_protocol, labels);
+ AddEdit(group, SETTING_SERVER_ADDRESS, 1010, SettingLevel::Basic, m_server, true);
+ std::shared_ptr<CSettingAction> subsetting = AddButton(group, SETTING_SERVER_BROWSE, 1024, SettingLevel::Basic, "", false);
+ if (subsetting != NULL)
+ subsetting->SetParent(SETTING_SERVER_ADDRESS);
+
+ AddEdit(group, SETTING_REMOTE_PATH, 1012, SettingLevel::Basic, m_path, true);
+ AddEdit(group, SETTING_PORT_NUMBER, 1013, SettingLevel::Basic, m_port, true);
+ AddEdit(group, SETTING_USERNAME, 1014, SettingLevel::Basic, m_username, true);
+ AddEdit(group, SETTING_PASSWORD, 15052, SettingLevel::Basic, m_password, true, true);
+}
+
+void CGUIDialogNetworkSetup::OnServerBrowse()
+{
+ // open a filebrowser dialog with the current address
+ VECSOURCES shares;
+ std::string path = ConstructPath();
+ // get the share as the base path
+ CMediaSource share;
+ std::string basePath = path;
+ std::string tempPath;
+ while (URIUtils::GetParentPath(basePath, tempPath))
+ basePath = tempPath;
+ share.strPath = basePath;
+ // don't include the user details in the share name
+ CURL url(share.strPath);
+ share.strName = url.GetWithoutUserDetails();
+ shares.push_back(share);
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(1015), path))
+ {
+ SetPath(path);
+ UpdateButtons();
+ }
+}
+
+void CGUIDialogNetworkSetup::OnOK()
+{
+ m_confirmed = true;
+ Close();
+}
+
+void CGUIDialogNetworkSetup::OnCancel()
+{
+ m_confirmed = false;
+ Close();
+}
+
+void CGUIDialogNetworkSetup::OnProtocolChange()
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_PROTOCOL);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), settingControl->GetID());
+ if (!OnMessage(msg))
+ return;
+ m_protocol = msg.GetParam1();
+ // set defaults for the port
+ m_port = std::to_string(m_protocols[m_protocol].defaultPort);
+
+ UpdateButtons();
+ }
+}
+
+void CGUIDialogNetworkSetup::UpdateButtons()
+{
+ // Address label
+ BaseSettingControlPtr addressControl = GetSettingControl(SETTING_SERVER_ADDRESS);
+ if (addressControl != NULL && addressControl->GetControl() != NULL)
+ {
+ int addressControlID = addressControl->GetID();
+ SET_CONTROL_LABEL2(addressControlID, m_server);
+ if (m_protocols[m_protocol].type == "smb")
+ {
+ SET_CONTROL_LABEL(addressControlID, 1010); // Server name
+ }
+ else
+ {
+ SET_CONTROL_LABEL(addressControlID, 1009); // Server Address
+ }
+ SendMessage(GUI_MSG_SET_TYPE, addressControlID, CGUIEditControl::INPUT_TYPE_TEXT, 1016);
+ }
+
+ // remote path
+ BaseSettingControlPtr pathControl = GetSettingControl(SETTING_REMOTE_PATH);
+ if (pathControl != NULL && pathControl->GetControl() != NULL)
+ {
+ int pathControlID = pathControl->GetID();
+ SET_CONTROL_LABEL2(pathControlID, m_path);
+ CONTROL_ENABLE_ON_CONDITION(pathControlID, m_protocols[m_protocol].supportPath);
+ if (m_protocols[m_protocol].type != "smb")
+ {
+ SET_CONTROL_LABEL(pathControlID, 1011); // Remote Path
+ }
+ else
+ {
+ SET_CONTROL_LABEL(pathControlID, 1012); // Shared Folder
+ }
+ SendMessage(GUI_MSG_SET_TYPE, pathControlID, CGUIEditControl::INPUT_TYPE_TEXT, 1017);
+ }
+
+ // username
+ BaseSettingControlPtr userControl = GetSettingControl(SETTING_USERNAME);
+ if (userControl != NULL && userControl->GetControl() != NULL)
+ {
+ int userControlID = userControl->GetID();
+ SET_CONTROL_LABEL2(userControlID, m_username);
+ CONTROL_ENABLE_ON_CONDITION(userControlID,
+ m_protocols[m_protocol].supportUsername);
+
+ SendMessage(GUI_MSG_SET_TYPE, userControlID, CGUIEditControl::INPUT_TYPE_TEXT, 1019);
+ }
+
+ // port
+ BaseSettingControlPtr portControl = GetSettingControl(SETTING_PORT_NUMBER);
+ if (portControl != NULL && portControl->GetControl() != NULL)
+ {
+ int portControlID = portControl->GetID();
+ SET_CONTROL_LABEL2(portControlID, m_port);
+ CONTROL_ENABLE_ON_CONDITION(portControlID, m_protocols[m_protocol].supportPort);
+
+ SendMessage(GUI_MSG_SET_TYPE, portControlID, CGUIEditControl::INPUT_TYPE_NUMBER, 1018);
+ }
+
+ // password
+ BaseSettingControlPtr passControl = GetSettingControl(SETTING_PASSWORD);
+ if (passControl != NULL && passControl->GetControl() != NULL)
+ {
+ int passControlID = passControl->GetID();
+ SET_CONTROL_LABEL2(passControlID, m_password);
+ CONTROL_ENABLE_ON_CONDITION(passControlID,
+ m_protocols[m_protocol].supportPassword);
+
+ SendMessage(GUI_MSG_SET_TYPE, passControlID, CGUIEditControl::INPUT_TYPE_PASSWORD, 12326);
+ }
+
+ // server browse should be disabled if we are in FTP, FTPS, HTTP, HTTPS, RSS, RSSS, DAV or DAVS
+ BaseSettingControlPtr browseControl = GetSettingControl(SETTING_SERVER_BROWSE);
+ if (browseControl != NULL && browseControl->GetControl() != NULL)
+ {
+ int browseControlID = browseControl->GetID();
+ CONTROL_ENABLE_ON_CONDITION(browseControlID,
+ m_protocols[m_protocol].supportBrowsing);
+ }
+}
+
+std::string CGUIDialogNetworkSetup::ConstructPath() const
+{
+ CURL url;
+ url.SetProtocol(m_protocols[m_protocol].type);
+
+ if (!m_username.empty())
+ {
+ // domain/name to domain\name
+ std::string username = m_username;
+ std::replace(username.begin(), username.end(), '/', '\\');
+
+ if (url.IsProtocol("smb") && username.find('\\') != std::string::npos)
+ {
+ auto pair = StringUtils::Split(username, "\\", 2);
+ url.SetDomain(pair[0]);
+ url.SetUserName(pair[1]);
+ }
+ else
+ url.SetUserName(m_username);
+ if (!m_password.empty())
+ url.SetPassword(m_password);
+ }
+
+ if (!m_server.empty())
+ url.SetHostName(m_server);
+
+ if (m_protocols[m_protocol].supportPort &&
+ !m_port.empty() && atoi(m_port.c_str()) > 0)
+ {
+ url.SetPort(atoi(m_port.c_str()));
+ }
+
+ if (!m_path.empty())
+ url.SetFileName(m_path);
+
+ return url.Get();
+}
+
+bool CGUIDialogNetworkSetup::SetPath(const std::string &path)
+{
+ UpdateAvailableProtocols();
+
+ if (path.empty())
+ {
+ Reset();
+ return true;
+ }
+
+ CURL url(path);
+ m_protocol = -1;
+ for (size_t i = 0; i < m_protocols.size(); ++i)
+ {
+ if (m_protocols[i].type == url.GetProtocol())
+ {
+ m_protocol = i;
+ break;
+ }
+ }
+ if (m_protocol == -1)
+ {
+ CLog::LogF(LOGERROR, "Asked to initialize for unknown path {}", path);
+ Reset();
+ return false;
+ }
+
+ if (!url.GetDomain().empty())
+ m_username = url.GetDomain() + "\\" + url.GetUserName();
+ else
+ m_username = url.GetUserName();
+ m_password = url.GetPassWord();
+ m_port = std::to_string(url.GetPort());
+ m_server = url.GetHostName();
+ m_path = url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(m_path);
+
+ return true;
+}
+
+void CGUIDialogNetworkSetup::Reset()
+{
+ m_username.clear();
+ m_password.clear();
+ m_port.clear();
+ m_server.clear();
+ m_path.clear();
+ m_protocol = 0;
+}
+
+void CGUIDialogNetworkSetup::UpdateAvailableProtocols()
+{
+ m_protocols.clear();
+#ifdef HAS_FILESYSTEM_SMB
+ // most popular protocol at the first place
+ m_protocols.emplace_back(Protocol{true, true, true, false, true, 0, "smb", 20171, ""});
+#endif
+ // protocols from vfs addon next
+ if (CServiceBroker::IsAddonInterfaceUp())
+ {
+ for (const auto& addon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ const auto& info = addon->GetProtocolInfo();
+ if (!addon->GetProtocolInfo().type.empty())
+ {
+ // only use first protocol
+ auto prots = StringUtils::Split(info.type, "|");
+ m_protocols.emplace_back(Protocol{
+ info.supportPath, info.supportUsername, info.supportPassword, info.supportPort,
+ info.supportBrowsing, info.defaultPort, prots.front(), info.label, addon->ID()});
+ }
+ }
+ }
+ // internals
+ const std::vector<Protocol> defaults = {{true, true, true, true, false, 443, "https", 20301, ""},
+ {true, true, true, true, false, 80, "http", 20300, ""},
+ {true, true, true, true, false, 443, "davs", 20254, ""},
+ {true, true, true, true, false, 80, "dav", 20253, ""},
+ {true, true, true, true, false, 21, "ftp", 20173, ""},
+ {true, true, true, true, false, 990, "ftps", 20174, ""},
+ {false, false, false, false, true, 0, "upnp", 20175, ""},
+ {true, true, true, true, false, 80, "rss", 20304, ""},
+ {true, true, true, true, false, 443, "rsss", 20305, ""}};
+
+ m_protocols.insert(m_protocols.end(), defaults.begin(), defaults.end());
+#ifdef HAS_FILESYSTEM_NFS
+ m_protocols.emplace_back(Protocol{true, false, false, false, true, 0, "nfs", 20259, ""});
+#endif
+}
diff --git a/xbmc/network/GUIDialogNetworkSetup.h b/xbmc/network/GUIDialogNetworkSetup.h
new file mode 100644
index 0000000..68de748
--- /dev/null
+++ b/xbmc/network/GUIDialogNetworkSetup.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+class CGUIDialogNetworkSetup : public CGUIDialogSettingsManualBase
+{
+public:
+ //! \brief A structure encapsulating properties of a supported protocol.
+ struct Protocol
+ {
+ bool supportPath; //!< Protocol has path in addition to server name
+ bool supportUsername; //!< Protocol uses logins
+ bool supportPassword; //!< Protocol supports passwords
+ bool supportPort; //!< Protocol supports port customization
+ bool supportBrowsing; //!< Protocol supports server browsing
+ int defaultPort; //!< Default port to use for protocol
+ std::string type; //!< URL type for protocol
+ int label; //!< String ID to use as label in dialog
+ std::string addonId; //!< Addon identifier, leaved empty if inside Kodi
+ };
+
+ CGUIDialogNetworkSetup(void);
+ ~CGUIDialogNetworkSetup(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ static bool ShowAndGetNetworkAddress(std::string &path);
+
+ std::string ConstructPath() const;
+ bool SetPath(const std::string &path);
+ bool IsConfirmed() const override { return m_confirmed; }
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override { return true; }
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ void OnProtocolChange();
+ void OnServerBrowse();
+ void OnOK();
+ void OnCancel() override;
+ void UpdateButtons();
+ void Reset();
+
+ void UpdateAvailableProtocols();
+
+ int m_protocol; //!< Currently selected protocol
+ std::vector<Protocol> m_protocols; //!< List of available protocols
+ std::string m_server;
+ std::string m_path;
+ std::string m_username;
+ std::string m_password;
+ std::string m_port;
+
+ bool m_confirmed;
+};
diff --git a/xbmc/network/IWSDiscovery.h b/xbmc/network/IWSDiscovery.h
new file mode 100644
index 0000000..47142dc
--- /dev/null
+++ b/xbmc/network/IWSDiscovery.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <memory>
+
+namespace WSDiscovery
+{
+class IWSDiscovery
+{
+public:
+ virtual ~IWSDiscovery() = default;
+ virtual bool StartServices() = 0;
+ virtual bool StopServices() = 0;
+ virtual bool IsRunning() = 0;
+
+ static std::unique_ptr<IWSDiscovery> GetInstance();
+};
+} // namespace WSDiscovery
diff --git a/xbmc/network/Network.cpp b/xbmc/network/Network.cpp
new file mode 100644
index 0000000..69a1513
--- /dev/null
+++ b/xbmc/network/Network.cpp
@@ -0,0 +1,525 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+#include "Network.h"
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/NetworkServices.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#ifdef TARGET_WINDOWS
+#include "platform/win32/WIN32Util.h"
+#include "utils/CharsetConverter.h"
+#endif
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+
+/* slightly modified in_ether taken from the etherboot project (http://sourceforge.net/projects/etherboot) */
+bool in_ether (const char *bufp, unsigned char *addr)
+{
+ if (strlen(bufp) != 17)
+ return false;
+
+ char c;
+ const char *orig;
+ unsigned char *ptr = addr;
+ unsigned val;
+
+ int i = 0;
+ orig = bufp;
+
+ while ((*bufp != '\0') && (i < 6))
+ {
+ val = 0;
+ c = *bufp++;
+
+ if (isdigit(c))
+ val = c - '0';
+ else if (c >= 'a' && c <= 'f')
+ val = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F')
+ val = c - 'A' + 10;
+ else
+ return false;
+
+ val <<= 4;
+ c = *bufp;
+ if (isdigit(c))
+ val |= c - '0';
+ else if (c >= 'a' && c <= 'f')
+ val |= c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F')
+ val |= c - 'A' + 10;
+ else if (c == ':' || c == '-' || c == 0)
+ val >>= 4;
+ else
+ return false;
+
+ if (c != 0)
+ bufp++;
+
+ *ptr++ = (unsigned char) (val & 0377);
+ i++;
+
+ if (*bufp == ':' || *bufp == '-')
+ bufp++;
+ }
+
+ if (bufp - orig != 17)
+ return false;
+
+ return true;
+}
+
+CNetworkBase::CNetworkBase() :
+ m_services(new CNetworkServices())
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_NETWORKMESSAGE, SERVICES_UP, 0);
+}
+
+CNetworkBase::~CNetworkBase()
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_NETWORKMESSAGE, SERVICES_DOWN, 0);
+}
+
+int CNetworkBase::ParseHex(char *str, unsigned char *addr)
+{
+ int len = 0;
+
+ while (*str)
+ {
+ int tmp;
+ if (str[1] == 0)
+ return -1;
+ if (sscanf(str, "%02x", (unsigned int *)&tmp) != 1)
+ return -1;
+ addr[len] = tmp;
+ len++;
+ str += 2;
+ }
+
+ return len;
+}
+
+bool CNetworkBase::GetHostName(std::string& hostname)
+{
+ char hostName[128];
+ if (gethostname(hostName, sizeof(hostName)))
+ return false;
+
+#ifdef TARGET_WINDOWS
+ std::string hostStr;
+ g_charsetConverter.systemToUtf8(hostName, hostStr);
+ hostname = hostStr;
+#else
+ hostname = hostName;
+#endif
+ return true;
+}
+
+bool CNetworkBase::IsLocalHost(const std::string& hostname)
+{
+ if (hostname.empty())
+ return false;
+
+ if (StringUtils::StartsWith(hostname, "127.")
+ || (hostname == "::1")
+ || StringUtils::EqualsNoCase(hostname, "localhost"))
+ return true;
+
+ std::string myhostname;
+ if (GetHostName(myhostname)
+ && StringUtils::EqualsNoCase(hostname, myhostname))
+ return true;
+
+ std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
+ std::vector<CNetworkInterface*>::const_iterator iter = ifaces.begin();
+ while (iter != ifaces.end())
+ {
+ CNetworkInterface* iface = *iter;
+ if (iface && iface->GetCurrentIPAddress() == hostname)
+ return true;
+
+ ++iter;
+ }
+
+ return false;
+}
+
+CNetworkInterface* CNetworkBase::GetFirstConnectedInterface()
+{
+ CNetworkInterface* fallbackInterface = nullptr;
+ for (CNetworkInterface* iface : GetInterfaceList())
+ {
+ if (iface && iface->IsConnected())
+ {
+ if (!iface->GetCurrentDefaultGateway().empty())
+ return iface;
+ else if (fallbackInterface == nullptr)
+ fallbackInterface = iface;
+ }
+ }
+
+ return fallbackInterface;
+}
+
+bool CNetworkBase::HasInterfaceForIP(unsigned long address)
+{
+ unsigned long subnet;
+ unsigned long local;
+ std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
+ std::vector<CNetworkInterface*>::const_iterator iter = ifaces.begin();
+ while (iter != ifaces.end())
+ {
+ CNetworkInterface* iface = *iter;
+ if (iface && iface->IsConnected())
+ {
+ subnet = ntohl(inet_addr(iface->GetCurrentNetmask().c_str()));
+ local = ntohl(inet_addr(iface->GetCurrentIPAddress().c_str()));
+ if( (address & subnet) == (local & subnet) )
+ return true;
+ }
+ ++iter;
+ }
+
+ return false;
+}
+
+bool CNetworkBase::IsAvailable(void)
+{
+ const std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
+ return (ifaces.size() != 0);
+}
+
+bool CNetworkBase::IsConnected()
+{
+ return GetFirstConnectedInterface() != NULL;
+}
+
+void CNetworkBase::NetworkMessage(EMESSAGE message, int param)
+{
+ switch( message )
+ {
+ case SERVICES_UP:
+ CLog::Log(LOGDEBUG, "{} - Starting network services", __FUNCTION__);
+ m_services->Start();
+ break;
+
+ case SERVICES_DOWN:
+ CLog::Log(LOGDEBUG, "{} - Signaling network services to stop", __FUNCTION__);
+ m_services->Stop(false); // tell network services to stop, but don't wait for them yet
+ CLog::Log(LOGDEBUG, "{} - Waiting for network services to stop", __FUNCTION__);
+ m_services->Stop(true); // wait for network services to stop
+ break;
+ }
+}
+
+bool CNetworkBase::WakeOnLan(const char* mac)
+{
+ int i, j, packet;
+ unsigned char ethaddr[8];
+ unsigned char buf [128];
+ unsigned char *ptr;
+
+ // Fetch the hardware address
+ if (!in_ether(mac, ethaddr))
+ {
+ CLog::Log(LOGERROR, "{} - Invalid hardware address specified ({})", __FUNCTION__, mac);
+ return false;
+ }
+
+ // Setup the socket
+ if ((packet = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to create socket ({})", __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ // Set socket options
+ struct sockaddr_in saddr;
+ saddr.sin_family = AF_INET;
+ saddr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+ saddr.sin_port = htons(9);
+
+ unsigned int value = 1;
+ if (setsockopt (packet, SOL_SOCKET, SO_BROADCAST, (char*) &value, sizeof( unsigned int ) ) == SOCKET_ERROR)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to set socket options ({})", __FUNCTION__, strerror(errno));
+ closesocket(packet);
+ return false;
+ }
+
+ // Build the magic packet (6 x 0xff + 16 x MAC address)
+ ptr = buf;
+ for (i = 0; i < 6; i++)
+ *ptr++ = 0xff;
+
+ for (j = 0; j < 16; j++)
+ for (i = 0; i < 6; i++)
+ *ptr++ = ethaddr[i];
+
+ // Send the magic packet
+ if (sendto (packet, (char *)buf, 102, 0, (struct sockaddr *)&saddr, sizeof (saddr)) < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to send magic packet ({})", __FUNCTION__, strerror(errno));
+ closesocket(packet);
+ return false;
+ }
+
+ closesocket(packet);
+ CLog::Log(LOGINFO, "{} - Magic packet send to '{}'", __FUNCTION__, mac);
+ return true;
+}
+
+// ping helper
+static const char* ConnectHostPort(SOCKET soc, const struct sockaddr_in& addr, struct timeval& timeOut, bool tryRead)
+{
+ // set non-blocking
+#ifdef TARGET_WINDOWS
+ u_long nonblocking = 1;
+ int result = ioctlsocket(soc, FIONBIO, &nonblocking);
+#else
+ int result = fcntl(soc, F_SETFL, fcntl(soc, F_GETFL) | O_NONBLOCK);
+#endif
+
+ if (result != 0)
+ return "set non-blocking option failed";
+
+ result = connect(soc, (const struct sockaddr *)&addr, sizeof(addr)); // non-blocking connect, will fail ..
+
+ if (result < 0)
+ {
+#ifdef TARGET_WINDOWS
+ if (WSAGetLastError() != WSAEWOULDBLOCK)
+#else
+ if (errno != EINPROGRESS)
+#endif
+ return "unexpected connect fail";
+
+ { // wait for connect to complete
+ fd_set wset;
+ FD_ZERO(&wset);
+ FD_SET(soc, &wset);
+
+ result = select(FD_SETSIZE, 0, &wset, 0, &timeOut);
+ }
+
+ if (result < 0)
+ return "select fail";
+
+ if (result == 0) // timeout
+ return ""; // no error
+
+ { // verify socket connection state
+ int err_code = -1;
+ socklen_t code_len = sizeof (err_code);
+
+ result = getsockopt(soc, SOL_SOCKET, SO_ERROR, (char*) &err_code, &code_len);
+
+ if (result != 0)
+ return "getsockopt fail";
+
+ if (err_code != 0)
+ return ""; // no error, just not connected
+ }
+ }
+
+ if (tryRead)
+ {
+ fd_set rset;
+ FD_ZERO(&rset);
+ FD_SET(soc, &rset);
+
+ result = select(FD_SETSIZE, &rset, 0, 0, &timeOut);
+
+ if (result > 0)
+ {
+ char message [32];
+
+ result = recv(soc, message, sizeof(message), 0);
+ }
+
+ if (result == 0)
+ return ""; // no reply yet
+
+ if (result < 0)
+ return "recv fail";
+ }
+
+ return 0; // success
+}
+
+bool CNetworkBase::PingHost(unsigned long ipaddr, unsigned short port, unsigned int timeOutMs, bool readability_check)
+{
+ if (port == 0) // use icmp ping
+ return PingHost (ipaddr, timeOutMs);
+
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = ipaddr;
+
+ SOCKET soc = socket(AF_INET, SOCK_STREAM, 0);
+
+ const char* err_msg = "invalid socket";
+
+ if (soc != INVALID_SOCKET)
+ {
+ struct timeval tmout;
+ tmout.tv_sec = timeOutMs / 1000;
+ tmout.tv_usec = (timeOutMs % 1000) * 1000;
+
+ err_msg = ConnectHostPort (soc, addr, tmout, readability_check);
+
+ (void) closesocket (soc);
+ }
+
+ if (err_msg && *err_msg)
+ {
+#ifdef TARGET_WINDOWS
+ std::string sock_err = CWIN32Util::WUSysMsg(WSAGetLastError());
+#else
+ std::string sock_err = strerror(errno);
+#endif
+
+ CLog::Log(LOGERROR, "{}({}:{}) - {} ({})", __FUNCTION__, inet_ntoa(addr.sin_addr), port,
+ err_msg, sock_err);
+ }
+
+ return err_msg == 0;
+}
+
+//creates, binds and listens tcp sockets on the desired port. Set bindLocal to
+//true to bind to localhost only.
+std::vector<SOCKET> CreateTCPServerSocket(const int port, const bool bindLocal, const int backlog, const char *callerName)
+{
+#ifdef WINSOCK_VERSION
+ int yes = 1;
+#else
+ unsigned int yes = 1;
+#endif
+
+ std::vector<SOCKET> sockets;
+ struct addrinfo* results = nullptr;
+
+ std::string sPort = std::to_string(port);
+ struct addrinfo hints = {};
+ hints.ai_family = PF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+ hints.ai_protocol = 0;
+
+ int error = getaddrinfo(bindLocal ? "localhost" : nullptr, sPort.c_str(), &hints, &results);
+ if (error != 0)
+ return sockets;
+
+ for (struct addrinfo* result = results; result != nullptr; result = result->ai_next)
+ {
+ SOCKET sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
+ if (sock == INVALID_SOCKET)
+ continue;
+
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof(yes));
+ setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char*>(&yes), sizeof(yes));
+
+ if (bind(sock, result->ai_addr, result->ai_addrlen) != 0)
+ {
+ closesocket(sock);
+ CLog::Log(LOGDEBUG, "{} Server: Failed to bind {} serversocket", callerName,
+ result->ai_family == AF_INET6 ? "IPv6" : "IPv4");
+ continue;
+ }
+
+ if (listen(sock, backlog) == 0)
+ sockets.push_back(sock);
+ else
+ {
+ closesocket(sock);
+ CLog::Log(LOGERROR, "{} Server: Failed to set listen", callerName);
+ }
+ }
+ freeaddrinfo(results);
+
+ if (sockets.empty())
+ CLog::Log(LOGERROR, "{} Server: Failed to create serversocket(s)", callerName);
+
+ return sockets;
+}
+
+void CNetworkBase::WaitForNet()
+{
+ const int timeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_WAITFORNETWORK);
+ if (timeout <= 0)
+ return; // wait for network is disabled
+
+ // check if we have at least one network interface to wait for
+ if (!IsAvailable())
+ return;
+
+ CLog::Log(LOGINFO, "{}: Waiting for a network interface to come up (Timeout: {} s)", __FUNCTION__,
+ timeout);
+
+ const static int intervalMs = 200;
+ const int numMaxTries = (timeout * 1000) / intervalMs;
+
+ for(int i=0; i < numMaxTries; ++i)
+ {
+ if (i > 0)
+ KODI::TIME::Sleep(std::chrono::milliseconds(intervalMs));
+
+ if (IsConnected())
+ {
+ CLog::Log(LOGINFO, "{}: A network interface is up after waiting {} ms", __FUNCTION__,
+ i * intervalMs);
+ return;
+ }
+ }
+
+ CLog::Log(LOGINFO, "{}: No network interface did come up within {} s... Giving up...",
+ __FUNCTION__, timeout);
+}
+
+std::string CNetworkBase::GetIpStr(const struct sockaddr* sa)
+{
+ std::string result;
+ if (!sa)
+ return result;
+
+ char buffer[INET6_ADDRSTRLEN] = {};
+ switch (sa->sa_family)
+ {
+ case AF_INET:
+ inet_ntop(AF_INET, &reinterpret_cast<const struct sockaddr_in *>(sa)->sin_addr, buffer, INET_ADDRSTRLEN);
+ break;
+ case AF_INET6:
+ inet_ntop(AF_INET6, &reinterpret_cast<const struct sockaddr_in6 *>(sa)->sin6_addr, buffer, INET6_ADDRSTRLEN);
+ break;
+ default:
+ return result;
+ }
+
+ result = buffer;
+ return result;
+}
+
+std::string CNetworkBase::GetMaskByPrefixLength(uint8_t prefixLength)
+{
+ if (prefixLength > 32)
+ return "";
+
+ struct sockaddr_in sa;
+ sa.sin_family = AF_INET;
+ sa.sin_addr.s_addr = htonl(~((1 << (32u - prefixLength)) - 1));;
+ return CNetworkBase::GetIpStr(reinterpret_cast<struct sockaddr*>(&sa));
+}
diff --git a/xbmc/network/Network.h b/xbmc/network/Network.h
new file mode 100644
index 0000000..e996e12
--- /dev/null
+++ b/xbmc/network/Network.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "settings/lib/ISettingCallback.h"
+
+#include "PlatformDefs.h"
+
+class CNetworkInterface
+{
+public:
+ virtual ~CNetworkInterface() = default;
+
+ virtual bool IsEnabled(void) const = 0;
+ virtual bool IsConnected(void) const = 0;
+
+ virtual std::string GetMacAddress(void) const = 0;
+ virtual void GetMacAddressRaw(char rawMac[6]) const = 0;
+
+ virtual bool GetHostMacAddress(unsigned long host, std::string& mac) const = 0;
+
+ virtual std::string GetCurrentIPAddress() const = 0;
+ virtual std::string GetCurrentNetmask() const = 0;
+ virtual std::string GetCurrentDefaultGateway(void) const = 0;
+};
+
+class CSettings;
+class CNetworkServices;
+struct sockaddr;
+
+class CNetworkBase
+{
+public:
+ enum EMESSAGE
+ {
+ SERVICES_UP,
+ SERVICES_DOWN
+ };
+
+ static std::unique_ptr<CNetworkBase> GetNetwork();
+
+ CNetworkBase();
+ virtual ~CNetworkBase();
+
+ // Get network services
+ CNetworkServices& GetServices() { return *m_services; }
+
+ // Return our hostname
+ virtual bool GetHostName(std::string& hostname);
+
+ // Return the list of interfaces
+ virtual std::vector<CNetworkInterface*>& GetInterfaceList(void) = 0;
+
+ // Return the first interface which is active
+ virtual CNetworkInterface* GetFirstConnectedInterface(void);
+
+ // Return true if there is a interface for the same network as address
+ bool HasInterfaceForIP(unsigned long address);
+
+ // Return true if there's at least one defined network interface
+ bool IsAvailable(void);
+
+ // Return true if there's at least one interface which is connected
+ bool IsConnected(void);
+
+ // Return true if the magic packet was send
+ bool WakeOnLan(const char* mac);
+
+ // Return true if host replies to ping
+ bool PingHost(unsigned long host,
+ unsigned short port,
+ unsigned int timeout_ms = 2000,
+ bool readability_check = false);
+ virtual bool PingHost(unsigned long host, unsigned int timeout_ms = 2000) = 0;
+
+ // Get/set the nameserver(s)
+ virtual std::vector<std::string> GetNameServers(void) = 0;
+
+ // callback from application controlled thread to handle any setup
+ void NetworkMessage(EMESSAGE message, int param);
+
+ static int ParseHex(char* str, unsigned char* addr);
+
+ // Return true if given name or ip address corresponds to localhost
+ bool IsLocalHost(const std::string& hostname);
+
+ // Waits for the first network interface to become available
+ void WaitForNet();
+
+ /*!
+ \brief IPv6/IPv4 compatible conversion of host IP address
+ \param struct sockaddr
+ \return Function converts binary structure sockaddr to std::string.
+ It can read sockaddr_in and sockaddr_in6, cast as (sockaddr*).
+ IPv4 address is returned in the format x.x.x.x (where x is 0-255),
+ IPv6 address is returned in it's canonised form.
+ On error (or no IPv6/v4 valid input) empty string is returned.
+ */
+ static std::string GetIpStr(const sockaddr* sa);
+
+ /*!
+ \brief convert prefix length of IPv4 address to IP mask representation
+ \param prefix length
+ \return
+ */
+ static std::string GetMaskByPrefixLength(uint8_t prefixLength);
+
+ std::unique_ptr<CNetworkServices> m_services;
+};
+
+//creates, binds and listens tcp sockets on the desired port. Set bindLocal to
+//true to bind to localhost only.
+std::vector<SOCKET> CreateTCPServerSocket(const int port, const bool bindLocal, const int backlog, const char *callerName);
+
diff --git a/xbmc/network/NetworkServices.cpp b/xbmc/network/NetworkServices.cpp
new file mode 100644
index 0000000..4009433
--- /dev/null
+++ b/xbmc/network/NetworkServices.cpp
@@ -0,0 +1,1267 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "NetworkServices.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "network/EventServer.h"
+#include "network/Network.h"
+#include "network/TCPServer.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/RssManager.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <utility>
+
+#ifdef TARGET_LINUX
+#include "Util.h"
+#endif
+#ifdef HAS_AIRPLAY
+#include "network/AirPlayServer.h"
+#endif // HAS_AIRPLAY
+
+#ifdef HAS_AIRTUNES
+#include "network/AirTunesServer.h"
+#endif // HAS_AIRTUNES
+
+#ifdef HAS_ZEROCONF
+#include "network/Zeroconf.h"
+#endif // HAS_ZEROCONF
+
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#endif // HAS_UPNP
+
+#ifdef HAS_WEB_SERVER
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPImageHandler.h"
+#include "network/httprequesthandler/HTTPImageTransformationHandler.h"
+#include "network/httprequesthandler/HTTPVfsHandler.h"
+#include "network/httprequesthandler/HTTPJsonRpcHandler.h"
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+#include "network/httprequesthandler/HTTPPythonHandler.h"
+#endif
+#include "network/httprequesthandler/HTTPWebinterfaceHandler.h"
+#include "network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h"
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+
+#if defined(HAS_FILESYSTEM_SMB)
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/network/WSDiscoveryWin32.h"
+#else // defined(TARGET_POSIX)
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+#endif
+#endif
+
+#if defined(TARGET_DARWIN_OSX)
+#include "platform/darwin/osx/XBMCHelper.h"
+#endif
+
+using namespace KODI::MESSAGING;
+using namespace JSONRPC;
+using namespace EVENTSERVER;
+#ifdef HAS_UPNP
+using namespace UPNP;
+#endif // HAS_UPNP
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+CNetworkServices::CNetworkServices()
+#ifdef HAS_WEB_SERVER
+ : m_webserver(*new CWebServer),
+ m_httpImageHandler(*new CHTTPImageHandler),
+ m_httpImageTransformationHandler(*new CHTTPImageTransformationHandler),
+ m_httpVfsHandler(*new CHTTPVfsHandler),
+ m_httpJsonRpcHandler(*new CHTTPJsonRpcHandler)
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ , m_httpPythonHandler(*new CHTTPPythonHandler)
+#endif
+ , m_httpWebinterfaceHandler(*new CHTTPWebinterfaceHandler)
+ , m_httpWebinterfaceAddonsHandler(*new CHTTPWebinterfaceAddonsHandler)
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+{
+#ifdef HAS_WEB_SERVER
+ m_webserver.RegisterRequestHandler(&m_httpImageHandler);
+ m_webserver.RegisterRequestHandler(&m_httpImageTransformationHandler);
+ m_webserver.RegisterRequestHandler(&m_httpVfsHandler);
+ m_webserver.RegisterRequestHandler(&m_httpJsonRpcHandler);
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ m_webserver.RegisterRequestHandler(&m_httpPythonHandler);
+#endif
+ m_webserver.RegisterRequestHandler(&m_httpWebinterfaceAddonsHandler);
+ m_webserver.RegisterRequestHandler(&m_httpWebinterfaceHandler);
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+ std::set<std::string> settingSet{
+ CSettings::SETTING_SERVICES_WEBSERVER,
+ CSettings::SETTING_SERVICES_WEBSERVERPORT,
+ CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION,
+ CSettings::SETTING_SERVICES_WEBSERVERUSERNAME,
+ CSettings::SETTING_SERVICES_WEBSERVERPASSWORD,
+ CSettings::SETTING_SERVICES_WEBSERVERSSL,
+ CSettings::SETTING_SERVICES_ZEROCONF,
+ CSettings::SETTING_SERVICES_AIRPLAY,
+ CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL,
+ CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT,
+ CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD,
+ CSettings::SETTING_SERVICES_AIRPLAYPASSWORD,
+ CSettings::SETTING_SERVICES_UPNP,
+ CSettings::SETTING_SERVICES_UPNPSERVER,
+ CSettings::SETTING_SERVICES_UPNPRENDERER,
+ CSettings::SETTING_SERVICES_UPNPCONTROLLER,
+ CSettings::SETTING_SERVICES_ESENABLED,
+ CSettings::SETTING_SERVICES_ESPORT,
+ CSettings::SETTING_SERVICES_ESALLINTERFACES,
+ CSettings::SETTING_SERVICES_ESINITIALDELAY,
+ CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY,
+ CSettings::SETTING_SMB_WINSSERVER,
+ CSettings::SETTING_SMB_WORKGROUP,
+ CSettings::SETTING_SMB_MINPROTOCOL,
+ CSettings::SETTING_SMB_MAXPROTOCOL,
+ CSettings::SETTING_SMB_LEGACYSECURITY,
+ CSettings::SETTING_SERVICES_WSDISCOVERY,
+ };
+ m_settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_settings->GetSettingsManager()->RegisterCallback(this, settingSet);
+}
+
+CNetworkServices::~CNetworkServices()
+{
+ m_settings->GetSettingsManager()->UnregisterCallback(this);
+#ifdef HAS_WEB_SERVER
+ m_webserver.UnregisterRequestHandler(&m_httpImageHandler);
+ delete &m_httpImageHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpImageTransformationHandler);
+ delete &m_httpImageTransformationHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpVfsHandler);
+ delete &m_httpVfsHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpJsonRpcHandler);
+ delete &m_httpJsonRpcHandler;
+ CJSONRPC::Cleanup();
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ m_webserver.UnregisterRequestHandler(&m_httpPythonHandler);
+ delete &m_httpPythonHandler;
+#endif
+ m_webserver.UnregisterRequestHandler(&m_httpWebinterfaceAddonsHandler);
+ delete &m_httpWebinterfaceAddonsHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpWebinterfaceHandler);
+ delete &m_httpWebinterfaceHandler;
+#endif // HAS_WEB_INTERFACE
+ delete &m_webserver;
+#endif // HAS_WEB_SERVER
+}
+
+bool CNetworkServices::OnSettingChanging(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return false;
+
+ const std::string &settingId = setting->GetId();
+#ifdef HAS_WEB_SERVER
+ // Ask user to confirm disabling the authentication requirement, but not when the configuration
+ // would be invalid when authentication was enabled (meaning that the change was triggered
+ // automatically)
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION &&
+ !std::static_pointer_cast<const CSettingBool>(setting)->GetValue() &&
+ (!m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER) ||
+ (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER) &&
+ !m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())) &&
+ HELPERS::ShowYesNoDialogText(19098, 36634) != DialogResponse::CHOICE_YES)
+ {
+ // Leave it as-is
+ return false;
+ }
+
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVER ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPORT ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERSSL ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERUSERNAME ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPASSWORD)
+ {
+ if (IsWebserverRunning() && !StopWebserver())
+ return false;
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ {
+ // Prevent changing to an invalid configuration
+ if ((settingId == CSettings::SETTING_SERVICES_WEBSERVER ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPASSWORD) &&
+ m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION) &&
+ m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ {
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION)
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{36636});
+ }
+ else
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{36635});
+ }
+ return false;
+ }
+
+ // Ask for confirmation when enabling the web server
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVER &&
+ HELPERS::ShowYesNoDialogText(19098, 36632) != DialogResponse::CHOICE_YES)
+ {
+ // Revert change, do not start server
+ return false;
+ }
+
+ if (!StartWebserver())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33101}, CVariant{33100});
+ return false;
+ }
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_ESPORT ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPORT)
+ return ValidatePort(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else
+#endif // HAS_WEB_SERVER
+
+#ifdef HAS_ZEROCONF
+ if (settingId == CSettings::SETTING_SERVICES_ZEROCONF)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ return StartZeroconf();
+#ifdef HAS_AIRPLAY
+ else
+ {
+ // cannot disable
+ if (IsAirPlayServerRunning() || IsAirTunesServerRunning())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1259}, CVariant{34303});
+ return false;
+ }
+
+ return StopZeroconf();
+ }
+#endif // HAS_AIRPLAY
+ }
+ else
+#endif // HAS_ZEROCONF
+
+#ifdef HAS_AIRPLAY
+ if (settingId == CSettings::SETTING_SERVICES_AIRPLAY)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+#ifdef HAS_ZEROCONF
+ // AirPlay needs zeroconf
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ZEROCONF))
+ {
+ HELPERS::ShowOKDialogText(CVariant{1273}, CVariant{34302});
+ return false;
+ }
+#endif //HAS_ZEROCONF
+
+ // note - airtunesserver has to start before airplay server (ios7 client detection bug)
+#ifdef HAS_AIRTUNES
+ if (!StartAirTunesServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1274}, CVariant{33100});
+ return false;
+ }
+#endif //HAS_AIRTUNES
+
+ if (!StartAirPlayServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1273}, CVariant{33100});
+ return false;
+ }
+ }
+ else
+ {
+ bool ret = true;
+#ifdef HAS_AIRTUNES
+ if (!StopAirTunesServer(true))
+ ret = false;
+#endif //HAS_AIRTUNES
+
+ if (!StopAirPlayServer(true))
+ ret = false;
+
+ if (!ret)
+ return false;
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ if (!StartAirPlayServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1273}, CVariant{33100});
+ return false;
+ }
+ }
+ else
+ {
+ if (!StopAirPlayServer(true))
+ return false;
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_AIRPLAYPASSWORD ||
+ settingId == CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD)
+ {
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ return false;
+
+ if (!CAirPlayServer::SetCredentials(m_settings->GetBool(CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD),
+ m_settings->GetString(CSettings::SETTING_SERVICES_AIRPLAYPASSWORD)))
+ return false;
+ }
+ else
+#endif //HAS_AIRPLAY
+
+#ifdef HAS_UPNP
+ if (settingId == CSettings::SETTING_SERVICES_UPNP)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ StartUPnPClient();
+ StartUPnPController();
+ StartUPnPServer();
+ StartUPnPRenderer();
+ }
+ else
+ {
+ StopUPnPRenderer();
+ StopUPnPServer();
+ StopUPnPController();
+ StopUPnPClient();
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_UPNPSERVER)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ if (!StartUPnPServer())
+ return false;
+
+ // always stop and restart the client and controller if necessary
+ StopUPnPClient();
+ StopUPnPController();
+ StartUPnPClient();
+ StartUPnPController();
+ }
+ else
+ return StopUPnPServer();
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_UPNPRENDERER)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ return StartUPnPRenderer();
+ else
+ return StopUPnPRenderer();
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_UPNPCONTROLLER)
+ {
+ // always stop and restart
+ StopUPnPController();
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ return StartUPnPController();
+ }
+ else
+#endif // HAS_UPNP
+
+ if (settingId == CSettings::SETTING_SERVICES_ESENABLED)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ bool result = true;
+ if (!StartEventServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33102}, CVariant{33100});
+ result = false;
+ }
+
+ if (!StartJSONRPCServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33103}, CVariant{33100});
+ result = false;
+ }
+ return result;
+ }
+ else
+ {
+ bool result = true;
+ result = StopEventServer(true, true);
+ result &= StopJSONRPCServer(false);
+ return result;
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_ESPORT)
+ {
+ // restart eventserver without asking user
+ if (!StopEventServer(true, false))
+ return false;
+
+ if (!StartEventServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33102}, CVariant{33100});
+ return false;
+ }
+
+#if defined(TARGET_DARWIN_OSX)
+ // reconfigure XBMCHelper for port changes
+ XBMCHelper::GetInstance().Configure();
+#endif // TARGET_DARWIN_OSX
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_ESALLINTERFACES)
+ {
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESALLINTERFACES) &&
+ HELPERS::ShowYesNoDialogText(19098, 36633) != DialogResponse::CHOICE_YES)
+ {
+ // Revert change, do not start server
+ return false;
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ {
+ if (!StopEventServer(true, true))
+ return false;
+
+ if (!StartEventServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33102}, CVariant{33100});
+ return false;
+ }
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ {
+ if (!StopJSONRPCServer(true))
+ return false;
+
+ if (!StartJSONRPCServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33103}, CVariant{33100});
+ return false;
+ }
+ }
+ }
+
+ else if (settingId == CSettings::SETTING_SERVICES_ESINITIALDELAY ||
+ settingId == CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY)
+ {
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return RefreshEventServer();
+ }
+
+#if defined(HAS_FILESYSTEM_SMB)
+ else if (settingId == CSettings::SETTING_SERVICES_WSDISCOVERY)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ if (!StartWSDiscovery())
+ return false;
+ }
+ else
+ return StopWSDiscovery();
+ }
+#endif // HAS_FILESYSTEM_SMB
+
+ return true;
+}
+
+void CNetworkServices::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_SMB_WINSSERVER ||
+ settingId == CSettings::SETTING_SMB_WORKGROUP ||
+ settingId == CSettings::SETTING_SMB_MINPROTOCOL ||
+ settingId == CSettings::SETTING_SMB_MAXPROTOCOL ||
+ settingId == CSettings::SETTING_SMB_LEGACYSECURITY)
+ {
+ // okey we really don't need to restart, only deinit samba, but that could be damn hard if something is playing
+ //! @todo - General way of handling setting changes that require restart
+ if (HELPERS::ShowYesNoDialogText(CVariant{14038}, CVariant{14039}) ==
+ DialogResponse::CHOICE_YES)
+ {
+ m_settings->Save();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESTARTAPP);
+ }
+ }
+}
+
+bool CNetworkServices::OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ if (setting == NULL)
+ return false;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERUSERNAME)
+ {
+ // if webserverusername is xbmc and pw is not empty we treat it as altered
+ // and don't change the username to kodi - part of rebrand
+ if (m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERUSERNAME) == "xbmc" &&
+ !m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ return true;
+ }
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERPORT)
+ {
+ // if webserverport is default but webserver is activated then treat it as altered
+ // and don't change the port to new value
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ return true;
+ }
+ return false;
+}
+
+void CNetworkServices::Start()
+{
+ StartZeroconf();
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ StartUPnP();
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED) && !StartEventServer())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33102), g_localizeStrings.Get(33100));
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED) && !StartJSONRPCServer())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33103), g_localizeStrings.Get(33100));
+
+#ifdef HAS_WEB_SERVER
+ // Start web server after eventserver and JSON-RPC server, so users can use these interfaces
+ // to confirm the warning message below if it is shown
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ {
+ // services.webserverauthentication setting was added in Kodi v18 and requires a valid password
+ // to be set, but on upgrade the setting will be activated automatically regardless of whether
+ // a password was set before -> this can lead to an invalid configuration
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION) &&
+ m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ {
+ // Alert user to new default security settings in new Kodi version
+ HELPERS::ShowOKDialogText(33101, 33104);
+ // Fix settings: Disable web server
+ m_settings->SetBool(CSettings::SETTING_SERVICES_WEBSERVER, false);
+ // Bring user to settings screen where authentication can be configured properly
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(
+ WINDOW_SETTINGS_SERVICE, std::vector<std::string>{"services.webserverauthentication"});
+ }
+ // Only try to start server if configuration is OK
+ else if (!StartWebserver())
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33101), g_localizeStrings.Get(33100));
+ }
+#endif // HAS_WEB_SERVER
+
+ // note - airtunesserver has to start before airplay server (ios7 client detection bug)
+ StartAirTunesServer();
+ StartAirPlayServer();
+ StartRss();
+ StartWSDiscovery();
+}
+
+void CNetworkServices::Stop(bool bWait)
+{
+ if (bWait)
+ {
+ StopUPnP(bWait);
+ StopZeroconf();
+ StopWebserver();
+ StopRss();
+ }
+
+ StopEventServer(bWait, false);
+ StopJSONRPCServer(bWait);
+ StopAirPlayServer(bWait);
+ StopAirTunesServer(bWait);
+ StopWSDiscovery();
+}
+
+bool CNetworkServices::StartServer(enum ESERVERS server, bool start)
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!settings)
+ return false;
+
+ bool ret = false;
+ switch (server)
+ {
+ case ES_WEBSERVER:
+ // the callback will take care of starting/stopping webserver
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_WEBSERVER, start);
+ break;
+
+ case ES_AIRPLAYSERVER:
+ // the callback will take care of starting/stopping airplay
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_AIRPLAY, start);
+ break;
+
+ case ES_JSONRPCSERVER:
+ // the callback will take care of starting/stopping jsonrpc server
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_ESENABLED, start);
+ break;
+
+ case ES_UPNPSERVER:
+ // the callback will take care of starting/stopping upnp server
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_UPNPSERVER, start);
+ break;
+
+ case ES_UPNPRENDERER:
+ // the callback will take care of starting/stopping upnp renderer
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_UPNPRENDERER, start);
+ break;
+
+ case ES_EVENTSERVER:
+ // the callback will take care of starting/stopping event server
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_ESENABLED, start);
+ break;
+
+ case ES_ZEROCONF:
+ // the callback will take care of starting/stopping zeroconf
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_ZEROCONF, start);
+ break;
+
+ case ES_WSDISCOVERY:
+ // the callback will take care of starting/stopping WS-Discovery
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_WSDISCOVERY, start);
+ break;
+
+ default:
+ ret = false;
+ break;
+ }
+ settings->Save();
+
+ return ret;
+}
+
+bool CNetworkServices::StartWebserver()
+{
+#ifdef HAS_WEB_SERVER
+ if (!CServiceBroker::GetNetwork().IsAvailable())
+ return false;
+
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ return false;
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION) &&
+ m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ {
+ CLog::Log(LOGERROR, "Tried to start webserver with invalid configuration (authentication "
+ "enabled, but no password set");
+ return false;
+ }
+
+ int webPort = m_settings->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT);
+ if (!ValidatePort(webPort))
+ {
+ CLog::Log(LOGERROR, "Cannot start Web Server on port {}", webPort);
+ return false;
+ }
+
+ if (IsWebserverRunning())
+ return true;
+
+ std::string username;
+ std::string password;
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION))
+ {
+ username = m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERUSERNAME);
+ password = m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD);
+ }
+
+ if (!m_webserver.Start(webPort, username, password))
+ return false;
+
+#ifdef HAS_ZEROCONF
+ std::vector<std::pair<std::string, std::string> > txt;
+ txt.emplace_back("txtvers", "1");
+ txt.emplace_back("uuid", CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_SERVICES_DEVICEUUID));
+
+ // publish web frontend and API services
+#ifdef HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->PublishService("servers.webserver", "_http._tcp", CSysInfo::GetDeviceName(), webPort, txt);
+#endif // HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->PublishService("servers.jsonrpc-http", "_xbmc-jsonrpc-h._tcp", CSysInfo::GetDeviceName(), webPort, txt);
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_WEB_SERVER
+ return false;
+}
+
+bool CNetworkServices::IsWebserverRunning()
+{
+#ifdef HAS_WEB_SERVER
+ return m_webserver.IsStarted();
+#endif // HAS_WEB_SERVER
+ return false;
+}
+
+bool CNetworkServices::StopWebserver()
+{
+#ifdef HAS_WEB_SERVER
+ if (!IsWebserverRunning())
+ return true;
+
+ if (!m_webserver.Stop() || m_webserver.IsStarted())
+ {
+ CLog::Log(LOGWARNING, "Webserver: Failed to stop.");
+ return false;
+ }
+
+#ifdef HAS_ZEROCONF
+#ifdef HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->RemoveService("servers.webserver");
+#endif // HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->RemoveService("servers.jsonrpc-http");
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_WEB_SERVER
+ return false;
+}
+
+bool CNetworkServices::StartAirPlayServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT))
+ return true;
+
+#ifdef HAS_AIRPLAY
+ if (!CServiceBroker::GetNetwork().IsAvailable() || !m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ return false;
+
+ if (IsAirPlayServerRunning())
+ return true;
+
+ if (!CAirPlayServer::StartServer(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_airPlayPort, true))
+ return false;
+
+ if (!CAirPlayServer::SetCredentials(m_settings->GetBool(CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD),
+ m_settings->GetString(CSettings::SETTING_SERVICES_AIRPLAYPASSWORD)))
+ return false;
+
+#ifdef HAS_ZEROCONF
+ std::vector<std::pair<std::string, std::string> > txt;
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ txt.emplace_back("deviceid", iface != nullptr ? iface->GetMacAddress() : "FF:FF:FF:FF:FF:F2");
+ txt.emplace_back("model", "Xbmc,1");
+ txt.emplace_back("srcvers", AIRPLAY_SERVER_VERSION_STR);
+
+ // for ios8 clients we need to announce mirroring support
+ // else we won't get video urls anymore.
+ // We also announce photo caching support (as it seems faster and
+ // we have implemented it anyways).
+ txt.emplace_back("features", "0x20F7");
+
+ CZeroconf::GetInstance()->PublishService("servers.airplay", "_airplay._tcp", CSysInfo::GetDeviceName(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_airPlayPort, txt);
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_AIRPLAY
+ return false;
+}
+
+bool CNetworkServices::IsAirPlayServerRunning()
+{
+#ifdef HAS_AIRPLAY
+ return CAirPlayServer::IsRunning();
+#endif // HAS_AIRPLAY
+ return false;
+}
+
+bool CNetworkServices::StopAirPlayServer(bool bWait)
+{
+#ifdef HAS_AIRPLAY
+ if (!IsAirPlayServerRunning())
+ return true;
+
+ CAirPlayServer::StopServer(bWait);
+
+#ifdef HAS_ZEROCONF
+ CZeroconf::GetInstance()->RemoveService("servers.airplay");
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_AIRPLAY
+ return false;
+}
+
+bool CNetworkServices::StartAirTunesServer()
+{
+#ifdef HAS_AIRTUNES
+ if (!CServiceBroker::GetNetwork().IsAvailable() || !m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ return false;
+
+ if (IsAirTunesServerRunning())
+ return true;
+
+ if (!CAirTunesServer::StartServer(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_airTunesPort, true,
+ m_settings->GetBool(CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD),
+ m_settings->GetString(CSettings::SETTING_SERVICES_AIRPLAYPASSWORD)))
+ {
+ CLog::Log(LOGERROR, "Failed to start AirTunes Server");
+ return false;
+ }
+
+ return true;
+#endif // HAS_AIRTUNES
+ return false;
+}
+
+bool CNetworkServices::IsAirTunesServerRunning()
+{
+#ifdef HAS_AIRTUNES
+ return CAirTunesServer::IsRunning();
+#endif // HAS_AIRTUNES
+ return false;
+}
+
+bool CNetworkServices::StopAirTunesServer(bool bWait)
+{
+#ifdef HAS_AIRTUNES
+ if (!IsAirTunesServerRunning())
+ return true;
+
+ CAirTunesServer::StopServer(bWait);
+ return true;
+#endif // HAS_AIRTUNES
+ return false;
+}
+
+bool CNetworkServices::StartJSONRPCServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return false;
+
+ if (IsJSONRPCServerRunning())
+ return true;
+
+ if (!CTCPServer::StartServer(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonTcpPort, m_settings->GetBool(CSettings::SETTING_SERVICES_ESALLINTERFACES)))
+ return false;
+
+#ifdef HAS_ZEROCONF
+ std::vector<std::pair<std::string, std::string> > txt;
+ txt.emplace_back("txtvers", "1");
+ txt.emplace_back("uuid", CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_SERVICES_DEVICEUUID));
+
+ CZeroconf::GetInstance()->PublishService("servers.jsonrpc-tpc", "_xbmc-jsonrpc._tcp", CSysInfo::GetDeviceName(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonTcpPort, txt);
+#endif // HAS_ZEROCONF
+
+ return true;
+}
+
+bool CNetworkServices::IsJSONRPCServerRunning()
+{
+ return CTCPServer::IsRunning();
+}
+
+bool CNetworkServices::StopJSONRPCServer(bool bWait)
+{
+ if (!IsJSONRPCServerRunning())
+ return true;
+
+ CTCPServer::StopServer(bWait);
+
+#ifdef HAS_ZEROCONF
+ CZeroconf::GetInstance()->RemoveService("servers.jsonrpc-tcp");
+#endif // HAS_ZEROCONF
+
+ return true;
+}
+
+bool CNetworkServices::StartEventServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return false;
+
+ if (IsEventServerRunning())
+ return true;
+
+ CEventServer* server = CEventServer::GetInstance();
+ if (!server)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory");
+ return false;
+ }
+
+ server->StartServer();
+
+ return true;
+}
+
+bool CNetworkServices::IsEventServerRunning()
+{
+ return CEventServer::GetInstance()->Running();
+}
+
+bool CNetworkServices::StopEventServer(bool bWait, bool promptuser)
+{
+ if (!IsEventServerRunning())
+ return true;
+
+ CEventServer* server = CEventServer::GetInstance();
+ if (!server)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory");
+ return false;
+ }
+
+ if (promptuser)
+ {
+ if (server->GetNumberOfClients() > 0)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{13140}, CVariant{13141}, CVariant{""}, CVariant{""},
+ 10000) != DialogResponse::CHOICE_YES)
+ {
+ CLog::Log(LOGINFO, "ES: Not stopping event server");
+ return false;
+ }
+ }
+ CLog::Log(LOGINFO, "ES: Stopping event server with confirmation");
+
+ CEventServer::GetInstance()->StopServer(true);
+ }
+ else
+ {
+ if (!bWait)
+ CLog::Log(LOGINFO, "ES: Stopping event server");
+
+ CEventServer::GetInstance()->StopServer(bWait);
+ }
+
+ return true;
+}
+
+bool CNetworkServices::RefreshEventServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return false;
+
+ if (!IsEventServerRunning())
+ return false;
+
+ CEventServer::GetInstance()->RefreshSettings();
+ return true;
+}
+
+bool CNetworkServices::StartUPnP()
+{
+ bool ret = false;
+#ifdef HAS_UPNP
+ ret |= StartUPnPClient();
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPSERVER))
+ {
+ ret |= StartUPnPServer();
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPCONTROLLER))
+ {
+ ret |= StartUPnPController();
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPRENDERER))
+ {
+ ret |= StartUPnPRenderer();
+ }
+#endif // HAS_UPNP
+ return ret;
+}
+
+bool CNetworkServices::StopUPnP(bool bWait)
+{
+#ifdef HAS_UPNP
+ if (!CUPnP::IsInstantiated())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp");
+ CUPnP::ReleaseInstance(bWait);
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPClient()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp client");
+ CUPnP::GetInstance()->StartClient();
+ return IsUPnPClientRunning();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPClientRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsClientStarted();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPClient()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPClientRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp client");
+ CUPnP::GetInstance()->StopClient();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPController()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPCONTROLLER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPSERVER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp controller");
+ CUPnP::GetInstance()->StartController();
+ return IsUPnPControllerRunning();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPControllerRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsControllerStarted();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPController()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPControllerRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp controller");
+ CUPnP::GetInstance()->StopController();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPRenderer()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPRENDERER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp renderer");
+ return CUPnP::GetInstance()->StartRenderer();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPRendererRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsInstantiated();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPRenderer()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPRendererRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp renderer");
+ CUPnP::GetInstance()->StopRenderer();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPServer()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPSERVER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp server");
+ return CUPnP::GetInstance()->StartServer();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPServerRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsInstantiated();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPServer()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPServerRunning())
+ return true;
+
+ StopUPnPController();
+
+ CLog::Log(LOGINFO, "stopping upnp server");
+ CUPnP::GetInstance()->StopServer();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartRss()
+{
+ if (IsRssRunning())
+ return true;
+
+ CRssManager::GetInstance().Start();
+ return true;
+}
+
+bool CNetworkServices::IsRssRunning()
+{
+ return CRssManager::GetInstance().IsActive();
+}
+
+bool CNetworkServices::StopRss()
+{
+ if (!IsRssRunning())
+ return true;
+
+ CRssManager::GetInstance().Stop();
+ return true;
+}
+
+bool CNetworkServices::StartZeroconf()
+{
+#ifdef HAS_ZEROCONF
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ZEROCONF))
+ return false;
+
+ if (IsZeroconfRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "starting zeroconf publishing");
+ return CZeroconf::GetInstance()->Start();
+#endif // HAS_ZEROCONF
+ return false;
+}
+
+bool CNetworkServices::IsZeroconfRunning()
+{
+#ifdef HAS_ZEROCONF
+ return CZeroconf::GetInstance()->IsStarted();
+#endif // HAS_ZEROCONF
+ return false;
+}
+
+bool CNetworkServices::StopZeroconf()
+{
+#ifdef HAS_ZEROCONF
+ if (!IsZeroconfRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping zeroconf publishing");
+ CZeroconf::GetInstance()->Stop();
+
+ return true;
+#endif // HAS_ZEROCONF
+ return false;
+}
+
+bool CNetworkServices::StartWSDiscovery()
+{
+#if defined(HAS_FILESYSTEM_SMB)
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_WSDISCOVERY))
+ return false;
+
+ if (IsWSDiscoveryRunning())
+ return true;
+
+ return CServiceBroker::GetWSDiscovery().StartServices();
+#endif // HAS_FILESYSTEM_SMB
+ return false;
+}
+
+bool CNetworkServices::IsWSDiscoveryRunning()
+{
+#if defined(HAS_FILESYSTEM_SMB)
+ return CServiceBroker::GetWSDiscovery().IsRunning();
+#endif // HAS_FILESYSTEM_SMB
+ return false;
+}
+
+bool CNetworkServices::StopWSDiscovery()
+{
+#if defined(HAS_FILESYSTEM_SMB)
+ if (!IsWSDiscoveryRunning())
+ return true;
+
+ CServiceBroker::GetWSDiscovery().StopServices();
+
+ return true;
+#endif // HAS_FILESYSTEM_SMB
+ return false;
+}
+
+bool CNetworkServices::ValidatePort(int port)
+{
+ if (port <= 0 || port > 65535)
+ return false;
+
+#ifdef TARGET_LINUX
+ if (!CUtil::CanBindPrivileged() && (port < 1024))
+ return false;
+#endif
+
+ return true;
+}
diff --git a/xbmc/network/NetworkServices.h b/xbmc/network/NetworkServices.h
new file mode 100644
index 0000000..bf8ca23
--- /dev/null
+++ b/xbmc/network/NetworkServices.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingCallback.h"
+
+class CSettings;
+#ifdef HAS_WEB_SERVER
+class CWebServer;
+class CHTTPImageHandler;
+class CHTTPImageTransformationHandler;
+class CHTTPVfsHandler;
+class CHTTPJsonRpcHandler;
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+class CHTTPPythonHandler;
+#endif
+class CHTTPWebinterfaceHandler;
+class CHTTPWebinterfaceAddonsHandler;
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+
+class CNetworkServices : public ISettingCallback
+{
+public:
+ CNetworkServices();
+ ~CNetworkServices() override;
+
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+
+ void Start();
+ void Stop(bool bWait);
+
+ enum ESERVERS
+ {
+ ES_WEBSERVER = 1,
+ ES_AIRPLAYSERVER,
+ ES_JSONRPCSERVER,
+ ES_UPNPRENDERER,
+ ES_UPNPSERVER,
+ ES_EVENTSERVER,
+ ES_ZEROCONF,
+ ES_WSDISCOVERY,
+ };
+
+ bool StartServer(enum ESERVERS server, bool start);
+
+ bool StartWebserver();
+ bool IsWebserverRunning();
+ bool StopWebserver();
+
+ bool StartAirPlayServer();
+ bool IsAirPlayServerRunning();
+ bool StopAirPlayServer(bool bWait);
+ bool StartAirTunesServer();
+ bool IsAirTunesServerRunning();
+ bool StopAirTunesServer(bool bWait);
+
+ bool StartJSONRPCServer();
+ bool IsJSONRPCServerRunning();
+ bool StopJSONRPCServer(bool bWait);
+
+ bool StartEventServer();
+ bool IsEventServerRunning();
+ bool StopEventServer(bool bWait, bool promptuser);
+ bool RefreshEventServer();
+
+ bool StartUPnP();
+ bool StopUPnP(bool bWait);
+ bool StartUPnPClient();
+ bool IsUPnPClientRunning();
+ bool StopUPnPClient();
+ bool StartUPnPController();
+ bool IsUPnPControllerRunning();
+ bool StopUPnPController();
+ bool StartUPnPRenderer();
+ bool IsUPnPRendererRunning();
+ bool StopUPnPRenderer();
+ bool StartUPnPServer();
+ bool IsUPnPServerRunning();
+ bool StopUPnPServer();
+
+ bool StartRss();
+ bool IsRssRunning();
+ bool StopRss();
+
+ bool StartZeroconf();
+ bool IsZeroconfRunning();
+ bool StopZeroconf();
+
+ bool StartWSDiscovery();
+ bool IsWSDiscoveryRunning();
+ bool StopWSDiscovery();
+
+private:
+ CNetworkServices(const CNetworkServices&);
+ CNetworkServices const& operator=(CNetworkServices const&);
+
+ bool ValidatePort(int port);
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+
+ // Network services
+#ifdef HAS_WEB_SERVER
+ CWebServer& m_webserver;
+ // Handlers
+ CHTTPImageHandler& m_httpImageHandler;
+ CHTTPImageTransformationHandler& m_httpImageTransformationHandler;
+ CHTTPVfsHandler& m_httpVfsHandler;
+ CHTTPJsonRpcHandler& m_httpJsonRpcHandler;
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ CHTTPPythonHandler& m_httpPythonHandler;
+#endif
+ CHTTPWebinterfaceHandler& m_httpWebinterfaceHandler;
+ CHTTPWebinterfaceAddonsHandler& m_httpWebinterfaceAddonsHandler;
+#endif
+#endif
+};
diff --git a/xbmc/network/Socket.cpp b/xbmc/network/Socket.cpp
new file mode 100644
index 0000000..f25e776
--- /dev/null
+++ b/xbmc/network/Socket.cpp
@@ -0,0 +1,329 @@
+/*
+ * Socket classes
+ * Copyright (c) 2008 d4rk
+ * Copyright (C) 2008-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Socket.h"
+
+#include "utils/ScopeGuard.h"
+#include "utils/log.h"
+
+#include <vector>
+
+using namespace SOCKETS;
+
+#ifdef WINSOCK_VERSION
+#define close closesocket
+#endif
+
+/**********************************************************************/
+/* CPosixUDPSocket */
+/**********************************************************************/
+
+bool CPosixUDPSocket::Bind(bool localOnly, int port, int range)
+{
+ // close any existing sockets
+ Close();
+
+ // If we can, create a socket that works with IPv6 and IPv4.
+ // If not, try an IPv4-only socket (we don't want to end up
+ // with an IPv6-only socket).
+ if (!localOnly) // Only bind loopback to ipv4. TODO : Implement dual bindinds.
+ {
+ m_ipv6Socket = CheckIPv6(port, range);
+
+ if (m_ipv6Socket)
+ {
+ m_iSock = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ if (m_iSock != INVALID_SOCKET)
+ {
+#ifdef WINSOCK_VERSION
+ const char zero = 0;
+#else
+ int zero = 0;
+#endif
+ if (setsockopt(m_iSock, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero)) == -1)
+ {
+ closesocket(m_iSock);
+ m_iSock = INVALID_SOCKET;
+ }
+ }
+ }
+ }
+
+ if (m_iSock == INVALID_SOCKET)
+ m_iSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+
+ if (m_iSock == INVALID_SOCKET)
+ {
+#ifdef TARGET_WINDOWS
+ int ierr = WSAGetLastError();
+ CLog::Log(LOGERROR, "UDP: Could not create socket {}", ierr);
+ // hack for broken third party libs
+ if (ierr == WSANOTINITIALISED)
+ {
+ WSADATA wd;
+ if (WSAStartup(MAKEWORD(2,2), &wd) != 0)
+ CLog::Log(LOGERROR, "UDP: WSAStartup failed");
+ }
+#else
+ CLog::Log(LOGERROR, "UDP: Could not create socket");
+#endif
+ CLog::Log(LOGERROR, "UDP: {}", strerror(errno));
+ return false;
+ }
+
+ // make sure we can reuse the address
+#ifdef WINSOCK_VERSION
+ const char yes=1;
+#else
+ int yes = 1;
+#endif
+ if (setsockopt(m_iSock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
+ {
+ CLog::Log(LOGWARNING, "UDP: Could not enable the address reuse options");
+ CLog::Log(LOGWARNING, "UDP: {}", strerror(errno));
+ }
+
+ // bind to any address or localhost
+ if (m_ipv6Socket)
+ {
+ if (localOnly)
+ m_addr = CAddress("::1");
+ else
+ m_addr = CAddress("::");
+ }
+ else
+ {
+ if (localOnly)
+ m_addr = CAddress("127.0.0.1");
+ else
+ m_addr = CAddress("0.0.0.0");
+ }
+
+ // bind the socket ( try from port to port+range )
+ for (m_iPort = port; m_iPort <= port + range; ++m_iPort)
+ {
+ if (m_ipv6Socket)
+ m_addr.saddr.saddr6.sin6_port = htons(m_iPort);
+ else
+ m_addr.saddr.saddr4.sin_port = htons(m_iPort);
+
+ if (bind(m_iSock, (struct sockaddr*)&m_addr.saddr, m_addr.size) != 0)
+ {
+ CLog::Log(LOGWARNING, "UDP: Error binding socket on port {} (ipv6 : {})", m_iPort,
+ m_ipv6Socket ? "true" : "false");
+ CLog::Log(LOGWARNING, "UDP: {}", strerror(errno));
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "UDP: Listening on port {} (ipv6 : {})", m_iPort,
+ m_ipv6Socket ? "true" : "false");
+ SetBound();
+ SetReady();
+ break;
+ }
+ }
+
+ // check for errors
+ if (!Bound())
+ {
+ CLog::Log(LOGERROR, "UDP: No suitable port found");
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+bool CPosixUDPSocket::CheckIPv6(int port, int range)
+{
+ CAddress testaddr("::");
+#if defined(TARGET_WINDOWS)
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<SOCKET, INVALID_SOCKET, decltype(closesocket)>;
+ CAutoPtrSocket testSocket(closesocket, socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP));
+#else
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<int, -1, decltype(close)>;
+ CAutoPtrSocket testSocket(close, socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP));
+#endif
+
+ if (static_cast<SOCKET>(testSocket) == INVALID_SOCKET)
+ {
+ CLog::LogF(LOGDEBUG, "Could not create IPv6 socket: {}", strerror(errno));
+ return false;
+ }
+
+#ifdef WINSOCK_VERSION
+ const char zero = 0;
+#else
+ int zero = 0;
+#endif
+
+ if (setsockopt(static_cast<SOCKET>(testSocket), IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero)) ==
+ -1)
+ {
+ CLog::LogF(LOGDEBUG, "Could not disable IPV6_V6ONLY for socket: {}", strerror(errno));
+ return false;
+ }
+
+ // Try to bind a socket to validate ipv6 status
+ for (; port <= port + range; port++)
+ {
+ testaddr.saddr.saddr6.sin6_port = htons(port);
+ if (bind(static_cast<SOCKET>(testSocket), reinterpret_cast<struct sockaddr*>(&testaddr.saddr),
+ testaddr.size) == 0)
+ {
+ CLog::LogF(LOGDEBUG, "IPv6 socket bound successfully");
+ return true;
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Could not bind IPv6 socket: {}", strerror(errno));
+ }
+ }
+
+ return false;
+}
+
+void CPosixUDPSocket::Close()
+{
+ if (m_iSock>=0)
+ {
+ close(m_iSock);
+ m_iSock = INVALID_SOCKET;
+ }
+ SetBound(false);
+ SetReady(false);
+}
+
+int CPosixUDPSocket::Read(CAddress& addr, const int buffersize, void *buffer)
+{
+ if (m_ipv6Socket)
+ addr.SetAddress("::");
+ return (int)recvfrom(m_iSock, (char*)buffer, (size_t)buffersize, 0,
+ (struct sockaddr*)&addr.saddr, &addr.size);
+}
+
+int CPosixUDPSocket::SendTo(const CAddress& addr, const int buffersize,
+ const void *buffer)
+{
+ return (int)sendto(m_iSock, (const char *)buffer, (size_t)buffersize, 0,
+ (const struct sockaddr*)&addr.saddr, addr.size);
+}
+
+/**********************************************************************/
+/* CSocketFactory */
+/**********************************************************************/
+
+std::unique_ptr<CUDPSocket> CSocketFactory::CreateUDPSocket()
+{
+ return std::make_unique<CPosixUDPSocket>();
+}
+
+/**********************************************************************/
+/* CSocketListener */
+/**********************************************************************/
+
+CSocketListener::CSocketListener()
+{
+ Clear();
+}
+
+void CSocketListener::AddSocket(CBaseSocket *sock)
+{
+ // WARNING: not threadsafe (which is ok for now)
+
+ if (sock && sock->Ready())
+ {
+ m_sockets.push_back(sock);
+ FD_SET(sock->Socket(), &m_fdset);
+#ifndef WINSOCK_VERSION
+ if (sock->Socket() > m_iMaxSockets)
+ m_iMaxSockets = sock->Socket();
+#endif
+ }
+}
+
+bool CSocketListener::Listen(int timeout)
+{
+ if (m_sockets.size()==0)
+ {
+ CLog::Log(LOGERROR, "SOCK: No sockets to listen for");
+ throw LISTENEMPTY;
+ }
+
+ m_iReadyCount = 0;
+ m_iCurrentSocket = 0;
+
+ FD_ZERO(&m_fdset);
+ for (unsigned int i = 0 ; i<m_sockets.size() ; i++)
+ {
+ FD_SET(m_sockets[i]->Socket(), &m_fdset);
+ }
+
+ // set our timeout
+ struct timeval tv;
+ int rem = timeout % 1000;
+ tv.tv_usec = rem * 1000;
+ tv.tv_sec = timeout / 1000;
+
+ m_iReadyCount = select(m_iMaxSockets+1, &m_fdset, NULL, NULL, (timeout < 0 ? NULL : &tv));
+
+ if (m_iReadyCount<0)
+ {
+ CLog::Log(LOGERROR, "SOCK: Error selecting socket(s)");
+ Clear();
+ throw LISTENERROR;
+ }
+ else
+ {
+ m_iCurrentSocket = 0;
+ return (m_iReadyCount>0);
+ }
+}
+
+void CSocketListener::Clear()
+{
+ m_sockets.clear();
+ FD_ZERO(&m_fdset);
+ m_iReadyCount = 0;
+ m_iMaxSockets = 0;
+ m_iCurrentSocket = 0;
+}
+
+CBaseSocket* CSocketListener::GetFirstReadySocket()
+{
+ if (m_iReadyCount<=0)
+ return NULL;
+
+ for (int i = 0 ; i < (int)m_sockets.size() ; i++)
+ {
+ if (FD_ISSET((m_sockets[i])->Socket(), &m_fdset))
+ {
+ m_iCurrentSocket = i;
+ return m_sockets[i];
+ }
+ }
+ return NULL;
+}
+
+CBaseSocket* CSocketListener::GetNextReadySocket()
+{
+ if (m_iReadyCount<=0)
+ return NULL;
+
+ for (int i = m_iCurrentSocket+1 ; i<(int)m_sockets.size() ; i++)
+ {
+ if (FD_ISSET(m_sockets[i]->Socket(), &m_fdset))
+ {
+ m_iCurrentSocket = i;
+ return m_sockets[i];
+ }
+ }
+ return NULL;
+}
diff --git a/xbmc/network/Socket.h b/xbmc/network/Socket.h
new file mode 100644
index 0000000..b945533
--- /dev/null
+++ b/xbmc/network/Socket.h
@@ -0,0 +1,253 @@
+/*
+ * Socket classes
+ * Copyright (c) 2008 d4rk
+ * Copyright (C) 2008-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string.h>
+#include <vector>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+#ifdef TARGET_POSIX
+typedef int SOCKET;
+#endif
+
+namespace SOCKETS
+{
+ // types of sockets
+ enum SocketType
+ {
+ ST_TCP,
+ ST_UDP,
+ ST_UNIX
+ };
+
+ /**********************************************************************/
+ /* IP address abstraction class */
+ /**********************************************************************/
+ class CAddress
+ {
+ public:
+ union
+ {
+ sockaddr_in saddr4;
+ sockaddr_in6 saddr6;
+ sockaddr saddr_generic;
+ } saddr;
+ socklen_t size;
+
+ public:
+ CAddress()
+ {
+ memset(&saddr, 0, sizeof(saddr));
+ saddr.saddr4.sin_family = AF_INET;
+ saddr.saddr4.sin_addr.s_addr = htonl(INADDR_ANY);
+ size = sizeof(saddr.saddr4);
+ }
+
+ explicit CAddress(const char *address)
+ {
+ SetAddress(address);
+ }
+
+ void SetAddress(const char *address)
+ {
+ in6_addr addr6;
+ memset(&saddr, 0, sizeof(saddr));
+ if (inet_pton(AF_INET6, address, &addr6) == 1)
+ {
+ saddr.saddr6.sin6_family = AF_INET6;
+ saddr.saddr6.sin6_addr = addr6;
+ size = sizeof(saddr.saddr6);
+ }
+ else
+ {
+ saddr.saddr4.sin_family = AF_INET;
+ saddr.saddr4.sin_addr.s_addr = inet_addr(address);
+ size = sizeof(saddr.saddr4);
+ }
+ }
+
+ // returns statically alloced buffer, do not free
+ const char *Address()
+ {
+ if (saddr.saddr_generic.sa_family == AF_INET6)
+ {
+ static char buf[INET6_ADDRSTRLEN];
+ return inet_ntop(AF_INET6, &saddr.saddr6.sin6_addr, buf, size);
+ }
+ else
+ return inet_ntoa(saddr.saddr4.sin_addr);
+ }
+
+ unsigned long ULong()
+ {
+ if (saddr.saddr_generic.sa_family == AF_INET6)
+ {
+ // IPv4 coercion (see http://home.samfundet.no/~sesse/ipv6-porting.pdf).
+ // We hash the entire IPv6 address because XBMC might conceivably need to
+ // distinguish between different hosts in the same subnet.
+ // This hash function (djbhash) is not too strong, but good enough.
+ uint32_t hash = 5381;
+ for (int i = 0; i < 16; ++i)
+ {
+ hash = hash * 33 + saddr.saddr6.sin6_addr.s6_addr[i];
+ }
+ // Move into 224.0.0.0/3. As a special safeguard, make sure we don't
+ // end up with the the special broadcast address 255.255.255.255.
+ hash |= 0xe0000000u;
+ if (hash == 0xffffffffu)
+ hash = 0xfffffffeu;
+ return (unsigned long)htonl(hash);
+ }
+ else
+ return (unsigned long)saddr.saddr4.sin_addr.s_addr;
+ }
+ };
+
+ /**********************************************************************/
+ /* Base class for all sockets */
+ /**********************************************************************/
+ class CBaseSocket
+ {
+ public:
+ CBaseSocket()
+ {
+ m_Type = ST_TCP;
+ m_bReady = false;
+ m_bBound = false;
+ m_iPort = 0;
+ }
+ virtual ~CBaseSocket() { Close(); }
+
+ // socket functions
+ virtual bool Bind(bool localOnly, int port, int range=0) = 0;
+ virtual bool Connect() = 0;
+ virtual void Close() {}
+
+ // state functions
+ bool Ready() { return m_bReady; }
+ bool Bound() { return m_bBound; }
+ SocketType Type() { return m_Type; }
+ int Port() { return m_iPort; }
+ virtual SOCKET Socket() = 0;
+
+ protected:
+ virtual void SetBound(bool set=true) { m_bBound = set; }
+ virtual void SetReady(bool set=true) { m_bReady = set; }
+
+ protected:
+ SocketType m_Type;
+ bool m_bReady;
+ bool m_bBound;
+ int m_iPort;
+ };
+
+ /**********************************************************************/
+ /* Base class for UDP socket implementations */
+ /**********************************************************************/
+ class CUDPSocket : public CBaseSocket
+ {
+ public:
+ CUDPSocket()
+ {
+ m_Type = ST_UDP;
+ }
+ // I/O functions
+ virtual int SendTo(const CAddress& addr, const int bufferlength,
+ const void* buffer) = 0;
+
+ // read datagrams, return no. of bytes read or -1 or error
+ virtual int Read(CAddress& addr, const int buffersize, void *buffer) = 0;
+ virtual bool Broadcast(const CAddress& addr, const int datasize,
+ const void* data) = 0;
+ };
+
+ // Implementation specific classes
+
+ /**********************************************************************/
+ /* POSIX based UDP socket implementation */
+ /**********************************************************************/
+ class CPosixUDPSocket : public CUDPSocket
+ {
+ public:
+ CPosixUDPSocket()
+ {
+ m_iSock = INVALID_SOCKET;
+ m_ipv6Socket = false;
+ }
+
+ bool Bind(bool localOnly, int port, int range=0) override;
+ bool Connect() override { return false; }
+ bool Listen(int timeout);
+ int SendTo(const CAddress& addr, const int datasize, const void* data) override;
+ int Read(CAddress& addr, const int buffersize, void *buffer) override;
+ bool Broadcast(const CAddress& addr, const int datasize, const void* data) override
+ {
+ //! @todo implement
+ return false;
+ }
+ SOCKET Socket() override { return m_iSock; }
+ void Close() override;
+
+ protected:
+ SOCKET m_iSock;
+ CAddress m_addr;
+
+ private:
+ bool CheckIPv6(int port, int range);
+
+ bool m_ipv6Socket;
+ };
+
+ /**********************************************************************/
+ /* Create and return platform dependent sockets */
+ /**********************************************************************/
+ class CSocketFactory
+ {
+ public:
+ static std::unique_ptr<CUDPSocket> CreateUDPSocket();
+ };
+
+ /**********************************************************************/
+ /* Listens on multiple sockets for reads */
+ /**********************************************************************/
+
+#define LISTENERROR 1
+#define LISTENEMPTY 2
+
+ class CSocketListener
+ {
+ public:
+ CSocketListener();
+ void AddSocket(CBaseSocket *);
+ bool Listen(int timeoutMs); // in ms, -1=>never timeout, 0=>poll
+ void Clear();
+ CBaseSocket* GetFirstReadySocket();
+ CBaseSocket* GetNextReadySocket();
+
+ protected:
+ std::vector<CBaseSocket*> m_sockets;
+ int m_iReadyCount;
+ int m_iMaxSockets;
+ int m_iCurrentSocket;
+ fd_set m_fdset;
+ };
+
+}
+
diff --git a/xbmc/network/TCPServer.cpp b/xbmc/network/TCPServer.cpp
new file mode 100644
index 0000000..cd055c4
--- /dev/null
+++ b/xbmc/network/TCPServer.cpp
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "TCPServer.h"
+
+#include "ServiceBroker.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "network/Network.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "websocket/WebSocketManager.h"
+
+#include <mutex>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <arpa/inet.h>
+#include <memory.h>
+#include <netinet/in.h>
+
+using namespace std::chrono_literals;
+
+#if defined(TARGET_WINDOWS) || defined(HAVE_LIBBLUETOOTH)
+static const char bt_service_name[] = "XBMC JSON-RPC";
+static const char bt_service_desc[] = "Interface for XBMC remote control over bluetooth";
+static const char bt_service_prov[] = "XBMC JSON-RPC Provider";
+static const uint32_t bt_service_guid[] = {0x65AE4CC0, 0x775D11E0, 0xBE16CE28, 0x4824019B};
+#endif
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/CharsetConverter.h"
+#endif
+
+#ifdef HAVE_LIBBLUETOOTH
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/rfcomm.h>
+#include <bluetooth/sdp.h>
+#include <bluetooth/sdp_lib.h>
+
+ /* The defines BDADDR_ANY and BDADDR_LOCAL are broken so use our own structs */
+static const bdaddr_t bt_bdaddr_any = {{0, 0, 0, 0, 0, 0}};
+static const bdaddr_t bt_bdaddr_local = {{0, 0, 0, 0xff, 0xff, 0xff}};
+
+#endif
+
+using namespace JSONRPC;
+
+#define RECEIVEBUFFER 4096
+
+namespace
+{
+constexpr size_t maxBufferLength = 64 * 1024;
+}
+
+CTCPServer *CTCPServer::ServerInstance = NULL;
+
+bool CTCPServer::StartServer(int port, bool nonlocal)
+{
+ StopServer(true);
+
+ ServerInstance = new CTCPServer(port, nonlocal);
+ if (ServerInstance->Initialize())
+ {
+ ServerInstance->Create(false);
+ return true;
+ }
+ else
+ return false;
+}
+
+void CTCPServer::StopServer(bool bWait)
+{
+ if (ServerInstance)
+ {
+ ServerInstance->StopThread(bWait);
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+ }
+}
+
+bool CTCPServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return ((CThread*)ServerInstance)->IsRunning();
+}
+
+CTCPServer::CTCPServer(int port, bool nonlocal) : CThread("TCPServer")
+{
+ m_port = port;
+ m_nonlocal = nonlocal;
+ m_sdpd = NULL;
+}
+
+void CTCPServer::Process()
+{
+ m_bStop = false;
+
+ while (!m_bStop)
+ {
+ SOCKET max_fd = 0;
+ fd_set rfds;
+ struct timeval to = {1, 0};
+ FD_ZERO(&rfds);
+
+ for (auto& it : m_servers)
+ {
+ FD_SET(it, &rfds);
+ if ((intptr_t)it > (intptr_t)max_fd)
+ max_fd = it;
+ }
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ FD_SET(m_connections[i]->m_socket, &rfds);
+ if ((intptr_t)m_connections[i]->m_socket > (intptr_t)max_fd)
+ max_fd = m_connections[i]->m_socket;
+ }
+
+ int res = select((intptr_t)max_fd+1, &rfds, NULL, NULL, &to);
+ if (res < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Select failed");
+ CThread::Sleep(1000ms);
+ Initialize();
+ }
+ else if (res > 0)
+ {
+ for (int i = m_connections.size() - 1; i >= 0; i--)
+ {
+ int socket = m_connections[i]->m_socket;
+ if (FD_ISSET(socket, &rfds))
+ {
+ char buffer[RECEIVEBUFFER] = {};
+ int nread = 0;
+ nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
+ bool close = false;
+ if (nread > 0)
+ {
+ std::string response;
+ if (m_connections[i]->IsNew())
+ {
+ CWebSocket *websocket = CWebSocketManager::Handle(buffer, nread, response);
+
+ if (!response.empty())
+ m_connections[i]->Send(response.c_str(), response.size());
+
+ if (websocket != NULL)
+ {
+ // Replace the CTCPClient with a CWebSocketClient
+ CWebSocketClient *websocketClient = new CWebSocketClient(websocket, *(m_connections[i]));
+ delete m_connections[i];
+ m_connections.erase(m_connections.begin() + i);
+ m_connections.insert(m_connections.begin() + i, websocketClient);
+ }
+ }
+
+ if (response.size() <= 0)
+ m_connections[i]->PushBuffer(this, buffer, nread);
+
+ close = m_connections[i]->Closing();
+ }
+ else
+ close = true;
+
+ if (close)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Disconnection detected");
+ m_connections[i]->Disconnect();
+ delete m_connections[i];
+ m_connections.erase(m_connections.begin() + i);
+ }
+ }
+ }
+
+ for (auto& it : m_servers)
+ {
+ if (FD_ISSET(it, &rfds))
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC Server: New connection detected");
+ CTCPClient *newconnection = new CTCPClient();
+ newconnection->m_socket =
+ accept(it, (sockaddr*)&newconnection->m_cliaddr, &newconnection->m_addrlen);
+
+ if (newconnection->m_socket == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Accept of new connection failed: {}", errno);
+ if (EBADF == errno)
+ {
+ CThread::Sleep(1000ms);
+ Initialize();
+ break;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: New connection added");
+ m_connections.push_back(newconnection);
+ }
+ }
+ }
+ }
+ }
+
+ Deinitialize();
+}
+
+bool CTCPServer::PrepareDownload(const char *path, CVariant &details, std::string &protocol)
+{
+ return false;
+}
+
+bool CTCPServer::Download(const char *path, CVariant &result)
+{
+ return false;
+}
+
+int CTCPServer::GetCapabilities()
+{
+ return Response | Announcing;
+}
+
+void CTCPServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (m_connections.empty())
+ return;
+
+ std::string str = IJSONRPCAnnouncer::AnnouncementToJSONRPC(flag, sender, message, data, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonOutputCompact);
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_connections[i]->m_critSection);
+ if ((m_connections[i]->GetAnnouncementFlags() & flag) == 0)
+ continue;
+ }
+
+ m_connections[i]->Send(str.c_str(), str.size());
+ }
+}
+
+bool CTCPServer::Initialize()
+{
+ Deinitialize();
+
+ bool started = false;
+
+ started |= InitializeBlue();
+ started |= InitializeTCP();
+
+ if (started)
+ {
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ CLog::Log(LOGINFO, "JSONRPC Server: Successfully initialized");
+ return true;
+ }
+ return false;
+}
+
+#ifdef TARGET_WINDOWS_STORE
+bool CTCPServer::InitializeBlue()
+{
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+ return true; // need to fake it for now
+}
+
+#else
+bool CTCPServer::InitializeBlue()
+{
+ if (!m_nonlocal)
+ return false;
+
+#ifdef TARGET_WINDOWS
+
+ SOCKET fd = socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM);
+ if (fd == INVALID_SOCKET)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to get bluetooth socket");
+ return false;
+ }
+ SOCKADDR_BTH sa = {};
+ sa.addressFamily = AF_BTH;
+ sa.port = BT_PORT_ANY;
+
+ if (bind(fd, (SOCKADDR*)&sa, sizeof(sa)) < 0)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to bind to bluetooth socket");
+ closesocket(fd);
+ return false;
+ }
+
+ ULONG optval = TRUE;
+ if (setsockopt(fd, SOL_RFCOMM, SO_BTH_AUTHENTICATE, (const char*)&optval, sizeof(optval)) == SOCKET_ERROR)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to force authentication for bluetooth socket");
+ closesocket(fd);
+ return false;
+ }
+
+ int len = sizeof(sa);
+ if (getsockname(fd, (SOCKADDR*)&sa, &len) < 0)
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to get bluetooth port");
+
+ if (listen(fd, 10) < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to listen to bluetooth port");
+ closesocket(fd);
+ return false;
+ }
+
+ m_servers.push_back(fd);
+
+ CSADDR_INFO addrinfo;
+ addrinfo.iProtocol = BTHPROTO_RFCOMM;
+ addrinfo.iSocketType = SOCK_STREAM;
+ addrinfo.LocalAddr.lpSockaddr = (SOCKADDR*)&sa;
+ addrinfo.LocalAddr.iSockaddrLength = sizeof(sa);
+ addrinfo.RemoteAddr.lpSockaddr = (SOCKADDR*)&sa;
+ addrinfo.RemoteAddr.iSockaddrLength = sizeof(sa);
+
+ using KODI::PLATFORM::WINDOWS::ToW;
+
+ WSAQUERYSET service = {};
+ service.dwSize = sizeof(service);
+ service.lpszServiceInstanceName = const_cast<LPWSTR>(ToW(bt_service_name).c_str());
+ service.lpServiceClassId = (LPGUID)&bt_service_guid;
+ service.lpszComment = const_cast<LPWSTR>(ToW(bt_service_desc).c_str());
+ service.dwNameSpace = NS_BTH;
+ service.lpNSProviderId = NULL; /* RFCOMM? */
+ service.lpcsaBuffer = &addrinfo;
+ service.dwNumberOfCsAddrs = 1;
+
+ if (WSASetService(&service, RNRSERVICE_REGISTER, 0) == SOCKET_ERROR)
+ CLog::Log(LOGERROR, "JSONRPC Server: failed to register bluetooth service error {}",
+ WSAGetLastError());
+
+ return true;
+#endif
+
+#ifdef HAVE_LIBBLUETOOTH
+
+ SOCKET fd = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
+ if (fd == INVALID_SOCKET)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to get bluetooth socket");
+ return false;
+ }
+ struct sockaddr_rc sa = {};
+ sa.rc_family = AF_BLUETOOTH;
+ sa.rc_bdaddr = bt_bdaddr_any;
+ sa.rc_channel = 0;
+
+ if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to bind to bluetooth socket");
+ closesocket(fd);
+ return false;
+ }
+
+ socklen_t len = sizeof(sa);
+ if (getsockname(fd, (struct sockaddr*)&sa, &len) < 0)
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to get bluetooth port");
+
+ if (listen(fd, 10) < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to listen to bluetooth port {}", sa.rc_channel);
+ closesocket(fd);
+ return false;
+ }
+
+ uint8_t rfcomm_channel = sa.rc_channel;
+
+ uuid_t root_uuid, l2cap_uuid, rfcomm_uuid, svc_uuid;
+ sdp_list_t *l2cap_list = 0,
+ *rfcomm_list = 0,
+ *root_list = 0,
+ *proto_list = 0,
+ *access_proto_list = 0,
+ *service_class = 0;
+
+ sdp_data_t *channel = 0;
+
+ sdp_record_t *record = sdp_record_alloc();
+
+ // set the general service ID
+ sdp_uuid128_create(&svc_uuid, &bt_service_guid);
+ sdp_set_service_id(record, svc_uuid);
+
+ // make the service record publicly browseable
+ sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
+ root_list = sdp_list_append(0, &root_uuid);
+ sdp_set_browse_groups(record, root_list);
+
+ // set l2cap information
+ sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
+ l2cap_list = sdp_list_append(0, &l2cap_uuid);
+ proto_list = sdp_list_append(0, l2cap_list);
+
+ // set rfcomm information
+ sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
+ channel = sdp_data_alloc(SDP_UINT8, &rfcomm_channel);
+ rfcomm_list = sdp_list_append(0, &rfcomm_uuid);
+ sdp_list_append(rfcomm_list, channel);
+ sdp_list_append(proto_list, rfcomm_list);
+
+ // attach protocol information to service record
+ access_proto_list = sdp_list_append(0, proto_list);
+ sdp_set_access_protos(record, access_proto_list);
+
+ // set the name, provider, and description
+ sdp_set_info_attr(record, bt_service_name, bt_service_prov, bt_service_desc);
+
+ // set the Service class ID
+ service_class = sdp_list_append(0, &svc_uuid);
+ sdp_set_service_classes(record, service_class);
+
+ // cleanup
+ sdp_data_free(channel);
+ sdp_list_free(l2cap_list, 0);
+ sdp_list_free(rfcomm_list, 0);
+ sdp_list_free(root_list, 0);
+ sdp_list_free(access_proto_list, 0);
+ sdp_list_free(service_class, 0);
+
+ // connect to the local SDP server, register the service record
+ sdp_session_t *session = sdp_connect(&bt_bdaddr_any, &bt_bdaddr_local, SDP_RETRY_IF_BUSY);
+ if (session == NULL)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to connect to sdpd");
+ closesocket(fd);
+ sdp_record_free(record);
+ return false;
+ }
+
+ if (sdp_record_register(session, record, 0) < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to register record with error {}", errno);
+ closesocket(fd);
+ sdp_close(session);
+ sdp_record_free(record);
+ return false;
+ }
+
+ m_sdpd = session;
+ m_servers.push_back(fd);
+
+ return true;
+#endif
+ return false;
+}
+#endif
+
+bool CTCPServer::InitializeTCP()
+{
+ Deinitialize();
+
+ std::vector<SOCKET> sockets = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "JSONRPC");
+ if (sockets.empty())
+ return false;
+
+ m_servers.insert(m_servers.end(), sockets.begin(), sockets.end());
+ return true;
+}
+
+void CTCPServer::Deinitialize()
+{
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ m_connections[i]->Disconnect();
+ delete m_connections[i];
+ }
+
+ m_connections.clear();
+
+ for (unsigned int i = 0; i < m_servers.size(); i++)
+ closesocket(m_servers[i]);
+
+ m_servers.clear();
+
+#ifdef HAVE_LIBBLUETOOTH
+ if (m_sdpd)
+ sdp_close((sdp_session_t*)m_sdpd);
+ m_sdpd = NULL;
+#endif
+
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+CTCPServer::CTCPClient::CTCPClient()
+{
+ m_new = true;
+ m_announcementflags = ANNOUNCEMENT::ANNOUNCE_ALL;
+ m_socket = INVALID_SOCKET;
+ m_beginBrackets = 0;
+ m_endBrackets = 0;
+ m_beginChar = 0;
+ m_endChar = 0;
+
+ m_addrlen = sizeof(m_cliaddr);
+}
+
+CTCPServer::CTCPClient::CTCPClient(const CTCPClient& client)
+{
+ Copy(client);
+}
+
+CTCPServer::CTCPClient& CTCPServer::CTCPClient::operator=(const CTCPClient& client)
+{
+ Copy(client);
+ return *this;
+}
+
+int CTCPServer::CTCPClient::GetPermissionFlags()
+{
+ return OPERATION_PERMISSION_ALL;
+}
+
+int CTCPServer::CTCPClient::GetAnnouncementFlags()
+{
+ return m_announcementflags;
+}
+
+bool CTCPServer::CTCPClient::SetAnnouncementFlags(int flags)
+{
+ m_announcementflags = flags;
+ return true;
+}
+
+void CTCPServer::CTCPClient::Send(const char *data, unsigned int size)
+{
+ unsigned int sent = 0;
+ do
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ sent += send(m_socket, data + sent, size - sent, 0);
+ } while (sent < size);
+}
+
+void CTCPServer::CTCPClient::PushBuffer(CTCPServer *host, const char *buffer, int length)
+{
+ m_new = false;
+ bool inObject = false;
+ bool inString = false;
+ bool escapeNext = false;
+
+ for (int i = 0; i < length; i++)
+ {
+ char c = buffer[i];
+
+ if (m_beginChar == 0 && c == '{')
+ {
+ m_beginChar = '{';
+ m_endChar = '}';
+ }
+ else if (m_beginChar == 0 && c == '[')
+ {
+ m_beginChar = '[';
+ m_endChar = ']';
+ }
+
+ if (m_beginChar != 0)
+ {
+ m_buffer.push_back(c);
+ if (inObject)
+ {
+ if (!inString)
+ {
+ if (c == '"')
+ inString = true;
+ }
+ else
+ {
+ if (escapeNext)
+ {
+ escapeNext = false;
+ }
+ else
+ {
+ if (c == '\\')
+ escapeNext = true;
+ else if (c == '"')
+ inString = false;
+ }
+ }
+ }
+ if (!inString)
+ {
+ if (c == m_beginChar)
+ {
+ m_beginBrackets++;
+ inObject = true;
+ }
+ else if (c == m_endChar)
+ {
+ m_endBrackets++;
+ if (m_beginBrackets == m_endBrackets)
+ inObject = false;
+ }
+ }
+ if (m_beginBrackets > 0 && m_endBrackets > 0 && m_beginBrackets == m_endBrackets)
+ {
+ std::string line = CJSONRPC::MethodCall(m_buffer, host, this);
+ Send(line.c_str(), line.size());
+ m_beginChar = m_beginBrackets = m_endBrackets = 0;
+ m_buffer.clear();
+ }
+ }
+ }
+}
+
+void CTCPServer::CTCPClient::Disconnect()
+{
+ if (m_socket > 0)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ shutdown(m_socket, SHUT_RDWR);
+ closesocket(m_socket);
+ m_socket = INVALID_SOCKET;
+ }
+}
+
+void CTCPServer::CTCPClient::Copy(const CTCPClient& client)
+{
+ m_new = client.m_new;
+ m_socket = client.m_socket;
+ m_cliaddr = client.m_cliaddr;
+ m_addrlen = client.m_addrlen;
+ m_announcementflags = client.m_announcementflags;
+ m_beginBrackets = client.m_beginBrackets;
+ m_endBrackets = client.m_endBrackets;
+ m_beginChar = client.m_beginChar;
+ m_endChar = client.m_endChar;
+ m_buffer = client.m_buffer;
+}
+
+CTCPServer::CWebSocketClient::CWebSocketClient(CWebSocket *websocket)
+{
+ m_websocket = websocket;
+ m_buffer.reserve(maxBufferLength);
+}
+
+CTCPServer::CWebSocketClient::CWebSocketClient(const CWebSocketClient& client)
+ : CTCPServer::CTCPClient(client)
+{
+ *this = client;
+ m_buffer.reserve(maxBufferLength);
+}
+
+CTCPServer::CWebSocketClient::CWebSocketClient(CWebSocket *websocket, const CTCPClient& client)
+{
+ Copy(client);
+
+ m_websocket = websocket;
+ m_buffer.reserve(maxBufferLength);
+}
+
+CTCPServer::CWebSocketClient::~CWebSocketClient()
+{
+ delete m_websocket;
+}
+
+CTCPServer::CWebSocketClient& CTCPServer::CWebSocketClient::operator=(const CWebSocketClient& client)
+{
+ Copy(client);
+
+ m_websocket = client.m_websocket;
+ m_buffer = client.m_buffer;
+
+ return *this;
+}
+
+void CTCPServer::CWebSocketClient::Send(const char *data, unsigned int size)
+{
+ const CWebSocketMessage *msg = m_websocket->Send(WebSocketTextFrame, data, size);
+ if (msg == NULL || !msg->IsComplete())
+ return;
+
+ std::vector<const CWebSocketFrame *> frames = msg->GetFrames();
+ for (unsigned int index = 0; index < frames.size(); index++)
+ CTCPClient::Send(frames.at(index)->GetFrameData(), (unsigned int)frames.at(index)->GetFrameLength());
+}
+
+void CTCPServer::CWebSocketClient::PushBuffer(CTCPServer *host, const char *buffer, int length)
+{
+ bool send;
+ const CWebSocketMessage *msg = NULL;
+
+ if (m_buffer.size() + length > maxBufferLength)
+ {
+ CLog::Log(LOGINFO, "WebSocket: client buffer size {} exceeded", maxBufferLength);
+ return Disconnect();
+ }
+
+ m_buffer.append(buffer, length);
+
+ const char* buf = m_buffer.data();
+ size_t len = m_buffer.size();
+
+ do
+ {
+ if ((msg = m_websocket->Handle(buf, len, send)) != NULL && msg->IsComplete())
+ {
+ std::vector<const CWebSocketFrame *> frames = msg->GetFrames();
+ if (send)
+ {
+ for (unsigned int index = 0; index < frames.size(); index++)
+ Send(frames.at(index)->GetFrameData(), (unsigned int)frames.at(index)->GetFrameLength());
+ }
+ else
+ {
+ for (unsigned int index = 0; index < frames.size(); index++)
+ CTCPClient::PushBuffer(host, frames.at(index)->GetApplicationData(), (int)frames.at(index)->GetLength());
+ }
+
+ delete msg;
+ }
+ }
+ while (len > 0 && msg != NULL);
+
+ if (len < m_buffer.size())
+ m_buffer = m_buffer.substr(m_buffer.size() - len);
+
+ if (m_websocket->GetState() == WebSocketStateClosed)
+ Disconnect();
+}
+
+void CTCPServer::CWebSocketClient::Disconnect()
+{
+ if (m_socket > 0)
+ {
+ if (m_websocket->GetState() != WebSocketStateClosed && m_websocket->GetState() != WebSocketStateNotConnected)
+ {
+ const CWebSocketFrame *closeFrame = m_websocket->Close();
+ if (closeFrame)
+ Send(closeFrame->GetFrameData(), (unsigned int)closeFrame->GetFrameLength());
+ }
+
+ if (m_websocket->GetState() == WebSocketStateClosed)
+ CTCPClient::Disconnect();
+ }
+}
diff --git a/xbmc/network/TCPServer.h b/xbmc/network/TCPServer.h
new file mode 100644
index 0000000..cf98194
--- /dev/null
+++ b/xbmc/network/TCPServer.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/json-rpc/IClient.h"
+#include "interfaces/json-rpc/IJSONRPCAnnouncer.h"
+#include "interfaces/json-rpc/ITransportLayer.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "websocket/WebSocket.h"
+
+#include <vector>
+
+#include <sys/socket.h>
+
+#include "PlatformDefs.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CTCPServer : public ITransportLayer, public JSONRPC::IJSONRPCAnnouncer, public CThread
+ {
+ public:
+ static bool StartServer(int port, bool nonlocal);
+ static void StopServer(bool bWait);
+ static bool IsRunning();
+
+ bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) override;
+ bool Download(const char *path, CVariant &result) override;
+ int GetCapabilities() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ protected:
+ void Process() override;
+ private:
+ CTCPServer(int port, bool nonlocal);
+ bool Initialize();
+ bool InitializeBlue();
+ bool InitializeTCP();
+ void Deinitialize();
+
+ class CTCPClient : public IClient
+ {
+ public:
+ CTCPClient();
+ //Copying a CCriticalSection is not allowed, so copy everything but that
+ //when adding a member variable, make sure to copy it in CTCPClient::Copy
+ CTCPClient(const CTCPClient& client);
+ CTCPClient& operator=(const CTCPClient& client);
+ ~CTCPClient() override = default;
+
+ int GetPermissionFlags() override;
+ int GetAnnouncementFlags() override;
+ bool SetAnnouncementFlags(int flags) override;
+
+ virtual void Send(const char *data, unsigned int size);
+ virtual void PushBuffer(CTCPServer *host, const char *buffer, int length);
+ virtual void Disconnect();
+
+ virtual bool IsNew() const { return m_new; }
+ virtual bool Closing() const { return false; }
+
+ SOCKET m_socket;
+ sockaddr_storage m_cliaddr;
+ socklen_t m_addrlen;
+ CCriticalSection m_critSection;
+
+ protected:
+ void Copy(const CTCPClient& client);
+ private:
+ bool m_new;
+ int m_announcementflags;
+ int m_beginBrackets, m_endBrackets;
+ char m_beginChar, m_endChar;
+ std::string m_buffer;
+ };
+
+ class CWebSocketClient : public CTCPClient
+ {
+ public:
+ explicit CWebSocketClient(CWebSocket *websocket);
+ CWebSocketClient(const CWebSocketClient& client);
+ CWebSocketClient(CWebSocket *websocket, const CTCPClient& client);
+ CWebSocketClient& operator=(const CWebSocketClient& client);
+ ~CWebSocketClient() override;
+
+ void Send(const char *data, unsigned int size) override;
+ void PushBuffer(CTCPServer *host, const char *buffer, int length) override;
+ void Disconnect() override;
+
+ bool IsNew() const override { return m_websocket == NULL; }
+ bool Closing() const override { return m_websocket != NULL && m_websocket->GetState() == WebSocketStateClosed; }
+
+ private:
+ CWebSocket *m_websocket;
+ std::string m_buffer;
+ };
+
+ std::vector<CTCPClient*> m_connections;
+ std::vector<SOCKET> m_servers;
+ int m_port;
+ bool m_nonlocal;
+ void* m_sdpd;
+
+ static CTCPServer *ServerInstance;
+ };
+}
diff --git a/xbmc/network/UdpClient.cpp b/xbmc/network/UdpClient.cpp
new file mode 100644
index 0000000..97ac4ed
--- /dev/null
+++ b/xbmc/network/UdpClient.cpp
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "UdpClient.h"
+
+#include <mutex>
+#ifdef TARGET_POSIX
+#include <sys/ioctl.h>
+#endif
+#include "Network.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <chrono>
+
+#include <arpa/inet.h>
+
+using namespace std::chrono_literals;
+
+#define UDPCLIENT_DEBUG_LEVEL LOGDEBUG
+
+CUdpClient::CUdpClient(void) : CThread("UDPClient")
+{}
+
+CUdpClient::~CUdpClient(void) = default;
+
+bool CUdpClient::Create(void)
+{
+ m_bStop = false;
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Creating UDP socket...");
+
+ // Create a UDP socket
+ client_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (client_socket == SOCKET_ERROR)
+ {
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Unable to create socket.");
+ return false;
+ }
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Setting broadcast socket option...");
+
+ unsigned int value = 1;
+ if ( setsockopt( client_socket, SOL_SOCKET, SO_BROADCAST, (char*) &value, sizeof( unsigned int ) ) == SOCKET_ERROR)
+ {
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Unable to set socket option.");
+ return false;
+ }
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Setting non-blocking socket options...");
+
+ unsigned long nonblocking = 1;
+ ioctlsocket(client_socket, FIONBIO, &nonblocking);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Spawning listener thread...");
+ CThread::Create(false);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Ready.");
+
+ return true;
+}
+
+void CUdpClient::Destroy()
+{
+ StopThread();
+ closesocket(client_socket);
+}
+
+void CUdpClient::OnStartup()
+{
+ SetPriority(ThreadPriority::LOWEST);
+}
+
+bool CUdpClient::Broadcast(int aPort, const std::string& aMessage)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(aPort);
+ addr.sin_addr.s_addr = INADDR_BROADCAST;
+ memset(&addr.sin_zero, 0, sizeof(addr.sin_zero));
+
+ UdpCommand broadcast = {addr, aMessage, NULL, 0};
+ commands.push_back(broadcast);
+
+ return true;
+}
+
+
+bool CUdpClient::Send(const std::string& aIpAddress, int aPort, const std::string& aMessage)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(aPort);
+ addr.sin_addr.s_addr = inet_addr(aIpAddress.c_str());
+ memset(&addr.sin_zero, 0, sizeof(addr.sin_zero));
+
+ UdpCommand transmit = {addr, aMessage, NULL, 0};
+ commands.push_back(transmit);
+
+ return true;
+}
+
+bool CUdpClient::Send(struct sockaddr_in aAddress, const std::string& aMessage)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ UdpCommand transmit = {aAddress, aMessage, NULL, 0};
+ commands.push_back(transmit);
+
+ return true;
+}
+
+bool CUdpClient::Send(struct sockaddr_in aAddress, unsigned char* pMessage, DWORD dwSize)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ UdpCommand transmit = {aAddress, "", pMessage, dwSize};
+ commands.push_back(transmit);
+
+ return true;
+}
+
+
+void CUdpClient::Process()
+{
+ CThread::Sleep(2000ms);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Listening.");
+
+ struct sockaddr_in remoteAddress;
+ char messageBuffer[1024];
+ DWORD dataAvailable;
+
+ while ( !m_bStop )
+ {
+ fd_set readset, exceptset;
+ FD_ZERO(&readset); FD_SET(client_socket, &readset);
+ FD_ZERO(&exceptset); FD_SET(client_socket, &exceptset);
+
+ int nfds = (int)(client_socket);
+ timeval tv = { 0, 100000 };
+ if (select(nfds, &readset, NULL, &exceptset, &tv) < 0)
+ {
+ CLog::Log(LOGERROR, "UDPCLIENT: failed to select on socket");
+ break;
+ }
+
+ // is there any data to read
+ dataAvailable = 0;
+ ioctlsocket(client_socket, FIONREAD, &dataAvailable);
+
+ // while there is data to read
+ while (dataAvailable > 0)
+ {
+ // read data
+ int messageLength = sizeof(messageBuffer) - 1 ;
+#ifndef TARGET_POSIX
+ int remoteAddressSize;
+#else
+ socklen_t remoteAddressSize;
+#endif
+ remoteAddressSize = sizeof(remoteAddress);
+
+ int ret = recvfrom(client_socket, messageBuffer, messageLength, 0, (struct sockaddr *) & remoteAddress, &remoteAddressSize);
+ if (ret != SOCKET_ERROR)
+ {
+ // Packet received
+ messageLength = ret;
+ messageBuffer[messageLength] = '\0';
+
+ std::string message = messageBuffer;
+
+ auto now = std::chrono::steady_clock::now();
+ auto timestamp =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT RX: {}\t\t<- '{}'", timestamp.count(), message);
+
+ OnMessage(remoteAddress, message, reinterpret_cast<unsigned char*>(messageBuffer), messageLength);
+ }
+ else
+ {
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Socket error {}", WSAGetLastError());
+ }
+
+ // is there any more data to read?
+ dataAvailable = 0;
+ ioctlsocket(client_socket, FIONREAD, &dataAvailable);
+ }
+
+ // dispatch a single command if any pending
+ while(DispatchNextCommand()) {}
+ }
+
+ closesocket(client_socket);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Stopped listening.");
+}
+
+
+bool CUdpClient::DispatchNextCommand()
+{
+ UdpCommand command;
+ {
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ if (commands.size() <= 0)
+ return false;
+
+ COMMANDITERATOR it = commands.begin();
+ command = *it;
+ commands.erase(it);
+ }
+
+ int ret;
+ if (command.binarySize > 0)
+ {
+ // only perform the following if logging level at debug
+
+ auto now = std::chrono::steady_clock::now();
+ auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL,
+ "UDPCLIENT TX: {}\t\t-> "
+ "<binary payload {} bytes>",
+ timestamp.count(), command.binarySize);
+
+ do
+ {
+ ret = sendto(client_socket, (const char*) command.binary, command.binarySize, 0, (struct sockaddr *) & command.address, sizeof(command.address));
+ }
+ while (ret == -1);
+
+ delete[] command.binary;
+ }
+ else
+ {
+ // only perform the following if logging level at debug
+ auto now = std::chrono::steady_clock::now();
+ auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT TX: {}\t\t-> '{}'", timestamp.count(),
+ command.message);
+
+ do
+ {
+ ret = sendto(client_socket, command.message.c_str(), command.message.size(), 0, (struct sockaddr *) & command.address, sizeof(command.address));
+ }
+ while (ret == -1 && !m_bStop);
+ }
+ return true;
+}
diff --git a/xbmc/network/UdpClient.h b/xbmc/network/UdpClient.h
new file mode 100644
index 0000000..060cb71
--- /dev/null
+++ b/xbmc/network/UdpClient.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ *
+ * Copyright (C) 2002-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <string>
+#include <vector>
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include "PlatformDefs.h"
+
+class CUdpClient : CThread
+{
+public:
+ CUdpClient();
+ ~CUdpClient(void) override;
+
+protected:
+
+ bool Create();
+ void Destroy();
+
+ void OnStartup() override;
+ void Process() override;
+
+ bool Broadcast(int aPort, const std::string& aMessage);
+ bool Send(const std::string& aIpAddress, int aPort, const std::string& aMessage);
+ bool Send(struct sockaddr_in aAddress, const std::string& aMessage);
+ bool Send(struct sockaddr_in aAddress, unsigned char* pMessage, DWORD dwSize);
+
+ virtual void OnMessage(struct sockaddr_in& aRemoteAddress,
+ const std::string& aMessage,
+ unsigned char* pMessage,
+ DWORD dwMessageLength)
+ {
+ }
+
+protected:
+
+ struct UdpCommand
+ {
+ struct sockaddr_in address;
+ std::string message;
+ unsigned char* binary;
+ DWORD binarySize;
+ };
+
+ bool DispatchNextCommand();
+
+ SOCKET client_socket;
+
+ std::vector<UdpCommand> commands;
+ typedef std::vector<UdpCommand> ::iterator COMMANDITERATOR;
+
+ CCriticalSection critical_section;
+};
diff --git a/xbmc/network/WakeOnAccess.cpp b/xbmc/network/WakeOnAccess.cpp
new file mode 100644
index 0000000..929c68b
--- /dev/null
+++ b/xbmc/network/WakeOnAccess.cpp
@@ -0,0 +1,956 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WakeOnAccess.h"
+
+#include "DNSNameCache.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <limits.h>
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#include <Platinum/Source/Platinum/Platinum.h>
+#endif
+
+#define DEFAULT_NETWORK_INIT_SEC (20) // wait 20 sec for network after startup or resume
+#define DEFAULT_NETWORK_SETTLE_MS (500) // require 500ms of consistent network availability before trusting it
+
+#define DEFAULT_TIMEOUT_SEC (5*60) // at least 5 minutes between each magic packets
+#define DEFAULT_WAIT_FOR_ONLINE_SEC_1 (40) // wait at 40 seconds after sending magic packet
+#define DEFAULT_WAIT_FOR_ONLINE_SEC_2 (40) // same for extended wait
+#define DEFAULT_WAIT_FOR_SERVICES_SEC (5) // wait 5 seconds after host go online to launch file sharing daemons
+
+using namespace std::chrono_literals;
+
+static CDateTime upnpInitReady;
+
+static int GetTotalSeconds(const CDateTimeSpan& ts)
+{
+ int hours = ts.GetHours() + ts.GetDays() * 24;
+ int minutes = ts.GetMinutes() + hours * 60;
+ return ts.GetSeconds() + minutes * 60;
+}
+
+static unsigned long HostToIP(const std::string& host)
+{
+ std::string ip;
+ CDNSNameCache::Lookup(host, ip);
+ return inet_addr(ip.c_str());
+}
+
+#define LOCALIZED(id) g_localizeStrings.Get(id)
+
+static void ShowDiscoveryMessage(const char* function, const char* server_name, bool new_entry)
+{
+ std::string message;
+
+ if (new_entry)
+ {
+ CLog::Log(LOGINFO, "{} - Create new entry for host '{}'", function, server_name);
+ message = StringUtils::Format(LOCALIZED(13035), server_name);
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "{} - Update existing entry for host '{}'", function, server_name);
+ message = StringUtils::Format(LOCALIZED(13034), server_name);
+ }
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, LOCALIZED(13033), message, 4000, true, 3000);
+}
+
+struct UPnPServer
+{
+ UPnPServer() : m_nextWake(CDateTime::GetCurrentDateTime()) {}
+ bool operator == (const UPnPServer& server) const { return server.m_uuid == m_uuid; }
+ bool operator != (const UPnPServer& server) const { return !(*this == server); }
+ bool operator == (const std::string& server_uuid) const { return server_uuid == m_uuid; }
+ bool operator != (const std::string& server_uuid) const { return !(*this == server_uuid); }
+ std::string m_name;
+ std::string m_uuid;
+ std::string m_mac;
+ CDateTime m_nextWake;
+};
+
+static UPnPServer* LookupUPnPServer(std::vector<UPnPServer>& list, const std::string& uuid)
+{
+ auto serverIt = find(list.begin(), list.end(), uuid);
+
+ return serverIt != list.end() ? &(*serverIt) : nullptr;
+}
+
+static void AddOrUpdateUPnPServer(std::vector<UPnPServer>& list, const UPnPServer& server)
+{
+ auto serverIt = find(list.begin(), list.end(), server);
+
+ bool addNewEntry = serverIt == list.end();
+
+ if (addNewEntry)
+ list.push_back(server); // add server
+ else
+ *serverIt = server; // update existing server
+
+ ShowDiscoveryMessage(__FUNCTION__, server.m_name.c_str(), addNewEntry);
+}
+
+static void AddMatchingUPnPServers(std::vector<UPnPServer>& list, const std::string& host, const std::string& mac, const CDateTimeSpan& wakeupDelay)
+{
+#ifdef HAS_UPNP
+ while (CDateTime::GetCurrentDateTime() < upnpInitReady)
+ KODI::TIME::Sleep(1s);
+
+ PLT_SyncMediaBrowser* browser = UPNP::CUPnP::GetInstance()->m_MediaBrowser;
+
+ if (browser)
+ {
+ UPnPServer server;
+ server.m_nextWake += wakeupDelay;
+
+ for (NPT_List<PLT_DeviceDataReference>::Iterator device = browser->GetMediaServers().GetFirstItem(); device; ++device)
+ {
+ if (host == (const char*) (*device)->GetURLBase().GetHost())
+ {
+ server.m_name = (*device)->GetFriendlyName();
+ server.m_uuid = (*device)->GetUUID();
+ server.m_mac = mac;
+
+ AddOrUpdateUPnPServer(list, server);
+ }
+ }
+ }
+#endif
+}
+
+static std::string LookupUPnPHost(const std::string& uuid)
+{
+#ifdef HAS_UPNP
+ UPNP::CUPnP* upnp = UPNP::CUPnP::GetInstance();
+
+ if (!upnp->IsClientStarted())
+ {
+ upnp->StartClient();
+
+ upnpInitReady = CDateTime::GetCurrentDateTime() + CDateTimeSpan(0, 0, 0, 10);
+ }
+
+ PLT_SyncMediaBrowser* browser = upnp->m_MediaBrowser;
+
+ PLT_DeviceDataReference device;
+
+ if (browser && NPT_SUCCEEDED(browser->FindServer(uuid.c_str(), device)) && !device.IsNull())
+ return (const char*)device->GetURLBase().GetHost();
+#endif
+
+ return "";
+}
+
+CWakeOnAccess::WakeUpEntry::WakeUpEntry (bool isAwake)
+ : timeout (0, 0, 0, DEFAULT_TIMEOUT_SEC)
+ , wait_online1_sec(DEFAULT_WAIT_FOR_ONLINE_SEC_1)
+ , wait_online2_sec(DEFAULT_WAIT_FOR_ONLINE_SEC_2)
+ , wait_services_sec(DEFAULT_WAIT_FOR_SERVICES_SEC)
+{
+ nextWake = CDateTime::GetCurrentDateTime();
+
+ if (isAwake)
+ nextWake += timeout;
+}
+
+//**
+
+class CMACDiscoveryJob : public CJob
+{
+public:
+ explicit CMACDiscoveryJob(const std::string& host) : m_host(host) {}
+
+ bool DoWork() override;
+
+ const std::string& GetMAC() const { return m_macAddress; }
+ const std::string& GetHost() const { return m_host; }
+
+private:
+ std::string m_macAddress;
+ std::string m_host;
+};
+
+bool CMACDiscoveryJob::DoWork()
+{
+ unsigned long ipAddress = HostToIP(m_host);
+
+ if (ipAddress == INADDR_NONE)
+ {
+ CLog::Log(LOGERROR, "{} - can't determine ip of '{}'", __FUNCTION__, m_host);
+ return false;
+ }
+
+ const std::vector<CNetworkInterface*>& ifaces = CServiceBroker::GetNetwork().GetInterfaceList();
+ for (const auto& it : ifaces)
+ {
+ if (it->GetHostMacAddress(ipAddress, m_macAddress))
+ return true;
+ }
+
+ return false;
+}
+
+//**
+
+class WaitCondition
+{
+public:
+ virtual ~WaitCondition() = default;
+ virtual bool SuccessWaiting () const { return false; }
+};
+
+//
+
+class NestDetect
+{
+public:
+ NestDetect() : m_gui_thread(CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ if (m_gui_thread)
+ ++m_nest;
+ }
+ ~NestDetect()
+ {
+ if (m_gui_thread)
+ m_nest--;
+ }
+ static int Level()
+ {
+ return m_nest;
+ }
+ bool IsNested() const
+ {
+ return m_gui_thread && m_nest > 1;
+ }
+
+private:
+ static int m_nest;
+ const bool m_gui_thread;
+};
+int NestDetect::m_nest = 0;
+
+//
+
+class ProgressDialogHelper
+{
+public:
+ explicit ProgressDialogHelper (const std::string& heading) : m_dialog(0)
+ {
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ m_dialog = gui->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ }
+
+ if (m_dialog)
+ {
+ m_dialog->SetHeading(CVariant{heading});
+ m_dialog->SetLine(0, CVariant{""});
+ m_dialog->SetLine(1, CVariant{""});
+ m_dialog->SetLine(2, CVariant{""});
+ }
+ }
+ ~ProgressDialogHelper ()
+ {
+ if (m_dialog)
+ m_dialog->Close();
+ }
+
+ bool HasDialog() const { return m_dialog != 0; }
+
+ enum wait_result { TimedOut, Canceled, Success };
+
+ wait_result ShowAndWait (const WaitCondition& waitObj, unsigned timeOutSec, const std::string& line1)
+ {
+ auto timeOutMs = std::chrono::milliseconds(timeOutSec * 1000);
+
+ if (m_dialog)
+ {
+ m_dialog->SetLine(0, CVariant{line1});
+
+ m_dialog->SetPercentage(1); // avoid flickering by starting at 1% ..
+ }
+
+ XbmcThreads::EndTime<> end_time(timeOutMs);
+
+ while (!end_time.IsTimePast())
+ {
+ if (waitObj.SuccessWaiting())
+ return Success;
+
+ if (m_dialog)
+ {
+ if (!m_dialog->IsActive())
+ m_dialog->Open();
+
+ if (m_dialog->IsCanceled())
+ return Canceled;
+
+ m_dialog->Progress();
+
+ auto ms_passed = timeOutMs - end_time.GetTimeLeft();
+
+ int percentage = (ms_passed.count() * 100) / timeOutMs.count();
+ m_dialog->SetPercentage(std::max(percentage, 1)); // avoid flickering , keep minimum 1%
+ }
+
+ KODI::TIME::Sleep(m_dialog ? 20ms : 200ms);
+ }
+
+ return TimedOut;
+ }
+
+private:
+ CGUIDialogProgress* m_dialog;
+};
+
+class NetworkStartWaiter : public WaitCondition
+{
+public:
+ NetworkStartWaiter (unsigned settle_time_ms, const std::string& host) : m_settle_time_ms (settle_time_ms), m_host(host)
+ {
+ }
+ bool SuccessWaiting () const override
+ {
+ unsigned long address = ntohl(HostToIP(m_host));
+ bool online = CServiceBroker::GetNetwork().HasInterfaceForIP(address);
+
+ if (!online) // setup endtime so we dont return true until network is consistently connected
+ m_end.Set(std::chrono::milliseconds(m_settle_time_ms));
+
+ return online && m_end.IsTimePast();
+ }
+private:
+ mutable XbmcThreads::EndTime<> m_end;
+ unsigned m_settle_time_ms;
+ const std::string m_host;
+};
+
+class PingResponseWaiter : public WaitCondition, private IJobCallback
+{
+public:
+ PingResponseWaiter (bool async, const CWakeOnAccess::WakeUpEntry& server)
+ : m_server(server), m_jobId(0), m_hostOnline(false)
+ {
+ if (async)
+ {
+ CJob* job = new CHostProberJob(server);
+ m_jobId = CServiceBroker::GetJobManager()->AddJob(job, this);
+ }
+ }
+ ~PingResponseWaiter() override { CServiceBroker::GetJobManager()->CancelJob(m_jobId); }
+ bool SuccessWaiting () const override
+ {
+ return m_jobId ? m_hostOnline : Ping(m_server);
+ }
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override
+ {
+ m_hostOnline = success;
+ }
+
+ static bool Ping(const CWakeOnAccess::WakeUpEntry& server, unsigned timeOutMs = 2000)
+ {
+ if (server.upnpUuid.empty())
+ {
+ unsigned long dst_ip = HostToIP(server.host);
+
+ return CServiceBroker::GetNetwork().PingHost(dst_ip, server.ping_port, timeOutMs, server.ping_mode & 1);
+ }
+ else // upnp mode
+ {
+ std::string host = LookupUPnPHost(server.upnpUuid);
+
+ if (host.empty())
+ {
+ KODI::TIME::Sleep(std::chrono::milliseconds(timeOutMs));
+
+ host = LookupUPnPHost(server.upnpUuid);
+ }
+
+ return !host.empty();
+ }
+ }
+
+private:
+ class CHostProberJob : public CJob
+ {
+ public:
+ explicit CHostProberJob(const CWakeOnAccess::WakeUpEntry& server) : m_server (server) {}
+
+ bool DoWork() override
+ {
+ while (!ShouldCancel(0,0))
+ {
+ if (PingResponseWaiter::Ping(m_server))
+ return true;
+ }
+ return false;
+ }
+
+ private:
+ const CWakeOnAccess::WakeUpEntry& m_server;
+ };
+
+ const CWakeOnAccess::WakeUpEntry& m_server;
+ unsigned int m_jobId;
+ bool m_hostOnline;
+};
+
+//
+
+CWakeOnAccess::CWakeOnAccess()
+ : m_netinit_sec(DEFAULT_NETWORK_INIT_SEC) // wait for network to connect
+ , m_netsettle_ms(DEFAULT_NETWORK_SETTLE_MS) // wait for network to settle
+{
+}
+
+CWakeOnAccess &CWakeOnAccess::GetInstance()
+{
+ static CWakeOnAccess sWakeOnAccess;
+ return sWakeOnAccess;
+}
+
+bool CWakeOnAccess::WakeUpHost(const CURL& url)
+{
+ const std::string& hostName = url.GetHostName();
+
+ if (!hostName.empty())
+ return WakeUpHost(hostName, url.Get(), url.IsProtocol("upnp"));
+
+ return true;
+}
+
+bool CWakeOnAccess::WakeUpHost(const std::string& hostName, const std::string& customMessage)
+{
+ return WakeUpHost(hostName, customMessage, false);
+}
+
+bool CWakeOnAccess::WakeUpHost(const std::string& hostName, const std::string& customMessage, bool upnpMode)
+{
+ if (!IsEnabled())
+ return true; // bail if feature is turned off
+
+ WakeUpEntry server;
+
+ if (FindOrTouchHostEntry(hostName, upnpMode, server))
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess [{}] triggered by accessing : {}", server.friendlyName,
+ customMessage);
+
+ NestDetect nesting ; // detect recursive calls on gui thread..
+
+ if (nesting.IsNested()) // we might get in trouble if it gets called back in loop
+ CLog::Log(LOGWARNING, "WakeOnAccess recursively called on gui-thread [{}]",
+ NestDetect::Level());
+
+ bool ret = WakeUpHost(server);
+
+ if (!ret) // extra log if we fail for some reason
+ CLog::Log(LOGWARNING, "WakeOnAccess failed to bring up [{}] - there may be trouble ahead !",
+ server.friendlyName);
+
+ TouchHostEntry(hostName, upnpMode);
+
+ return ret;
+ }
+ return true;
+}
+
+bool CWakeOnAccess::WakeUpHost(const WakeUpEntry& server)
+{
+ std::string heading = StringUtils::Format(LOCALIZED(13027), server.friendlyName);
+
+ ProgressDialogHelper dlg (heading);
+
+ {
+ NetworkStartWaiter waitObj (m_netsettle_ms, server.host); // wait until network connected before sending wake-on-lan
+
+ if (dlg.ShowAndWait (waitObj, m_netinit_sec, LOCALIZED(13028)) != ProgressDialogHelper::Success)
+ {
+ if (CServiceBroker::GetNetwork().IsConnected() && HostToIP(server.host) == INADDR_NONE)
+ {
+ // network connected (at least one interface) but dns-lookup failed (host by name, not ip-address), so dont abort yet
+ CLog::Log(LOGWARNING, "WakeOnAccess timeout/cancel while waiting for network (proceeding anyway)");
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess timeout/cancel while waiting for network");
+ return false; // timedout or canceled ; give up
+ }
+ }
+ }
+
+ if (PingResponseWaiter::Ping(server, 500)) // quick ping with short timeout to not block too long
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess success exit, server already running");
+ return true;
+ }
+
+ if (!CServiceBroker::GetNetwork().WakeOnLan(server.mac.c_str()))
+ {
+ CLog::Log(LOGERROR,"WakeOnAccess failed to send. (Is it blocked by firewall?)");
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread() || !appPlayer->IsPlaying())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, LOCALIZED(13029));
+ return false;
+ }
+
+ {
+ PingResponseWaiter waitObj (dlg.HasDialog(), server); // wait for ping response ..
+
+ ProgressDialogHelper::wait_result
+ result = dlg.ShowAndWait (waitObj, server.wait_online1_sec, LOCALIZED(13030));
+
+ if (result == ProgressDialogHelper::TimedOut)
+ result = dlg.ShowAndWait (waitObj, server.wait_online2_sec, LOCALIZED(13031));
+
+ if (result != ProgressDialogHelper::Success)
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess timeout/cancel while waiting for response");
+ return false; // timedout or canceled
+ }
+ }
+
+ // we have ping response ; just add extra wait-for-services before returning if requested
+
+ {
+ WaitCondition waitObj ; // wait uninterruptible fixed time for services ..
+
+ dlg.ShowAndWait (waitObj, server.wait_services_sec, LOCALIZED(13032));
+
+ CLog::Log(LOGINFO, "WakeOnAccess sequence completed, server started");
+ }
+ return true;
+}
+
+bool CWakeOnAccess::FindOrTouchHostEntry(const std::string& hostName, bool upnpMode, WakeUpEntry& result)
+{
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ bool need_wakeup = false;
+
+ UPnPServer* upnp = upnpMode ? LookupUPnPServer(m_UPnPServers, hostName) : nullptr;
+
+ for (auto& server : m_entries)
+ {
+ if (upnp ? StringUtils::EqualsNoCase(upnp->m_mac, server.mac) : StringUtils::EqualsNoCase(hostName, server.host))
+ {
+ CDateTime now = CDateTime::GetCurrentDateTime();
+
+ if (now >= (upnp ? upnp->m_nextWake : server.nextWake))
+ {
+ result = server;
+
+ result.friendlyName = upnp ? upnp->m_name : server.host;
+
+ if (upnp)
+ result.upnpUuid = upnp->m_uuid;
+
+ need_wakeup = true;
+ }
+ else // 'touch' next wakeup time
+ {
+ server.nextWake = now + server.timeout;
+
+ if (upnp)
+ upnp->m_nextWake = server.nextWake;
+ }
+
+ break;
+ }
+ }
+
+ return need_wakeup;
+}
+
+void CWakeOnAccess::TouchHostEntry(const std::string& hostName, bool upnpMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ UPnPServer* upnp = upnpMode ? LookupUPnPServer(m_UPnPServers, hostName) : nullptr;
+
+ for (auto& server : m_entries)
+ {
+ if (upnp ? StringUtils::EqualsNoCase(upnp->m_mac, server.mac) : StringUtils::EqualsNoCase(hostName, server.host))
+ {
+ server.nextWake = CDateTime::GetCurrentDateTime() + server.timeout;
+
+ if (upnp)
+ upnp->m_nextWake = server.nextWake;
+
+ return;
+ }
+ }
+}
+
+static void AddHost (const std::string& host, std::vector<std::string>& hosts)
+{
+ for (const auto& it : hosts)
+ if (StringUtils::EqualsNoCase(host, it))
+ return; // already there ..
+
+ if (!host.empty())
+ hosts.push_back(host);
+}
+
+static void AddHostFromDatabase(const DatabaseSettings& setting, std::vector<std::string>& hosts)
+{
+ if (StringUtils::EqualsNoCase(setting.type, "mysql"))
+ AddHost(setting.host, hosts);
+}
+
+void CWakeOnAccess::QueueMACDiscoveryForHost(const std::string& host)
+{
+ if (IsEnabled())
+ {
+ if (URIUtils::IsHostOnLAN(host, true))
+ CServiceBroker::GetJobManager()->AddJob(new CMACDiscoveryJob(host), this);
+ else
+ CLog::Log(LOGINFO, "{} - skip Mac discovery for non-local host '{}'", __FUNCTION__, host);
+ }
+}
+
+static void AddHostsFromMediaSource(const CMediaSource& source, std::vector<std::string>& hosts)
+{
+ for (const auto& it : source.vecPaths)
+ {
+ CURL url(it);
+
+ std::string host_name = url.GetHostName();
+
+ if (url.IsProtocol("upnp"))
+ host_name = LookupUPnPHost(host_name);
+
+ AddHost(host_name, hosts);
+ }
+}
+
+static void AddHostsFromVecSource(const VECSOURCES& sources, std::vector<std::string>& hosts)
+{
+ for (const auto& it : sources)
+ AddHostsFromMediaSource(it, hosts);
+}
+
+static void AddHostsFromVecSource(const VECSOURCES* sources, std::vector<std::string>& hosts)
+{
+ if (sources)
+ AddHostsFromVecSource(*sources, hosts);
+}
+
+void CWakeOnAccess::QueueMACDiscoveryForAllRemotes()
+{
+ std::vector<std::string> hosts;
+
+ // add media sources
+ CMediaSourceSettings& ms = CMediaSourceSettings::GetInstance();
+
+ AddHostsFromVecSource(ms.GetSources("video"), hosts);
+ AddHostsFromVecSource(ms.GetSources("music"), hosts);
+ AddHostsFromVecSource(ms.GetSources("files"), hosts);
+ AddHostsFromVecSource(ms.GetSources("pictures"), hosts);
+ AddHostsFromVecSource(ms.GetSources("programs"), hosts);
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // add mysql servers
+ AddHostFromDatabase(advancedSettings->m_databaseVideo, hosts);
+ AddHostFromDatabase(advancedSettings->m_databaseMusic, hosts);
+ AddHostFromDatabase(advancedSettings->m_databaseEpg, hosts);
+ AddHostFromDatabase(advancedSettings->m_databaseTV, hosts);
+
+ // add from path substitutions ..
+ for (const auto& pathPair : advancedSettings->m_pathSubstitutions)
+ {
+ CURL url(pathPair.second);
+ AddHost (url.GetHostName(), hosts);
+ }
+
+ for (const std::string& host : hosts)
+ QueueMACDiscoveryForHost(host);
+}
+
+void CWakeOnAccess::SaveMACDiscoveryResult(const std::string& host, const std::string& mac)
+{
+ CLog::Log(LOGINFO, "{} - Mac discovered for host '{}' -> '{}'", __FUNCTION__, host, mac);
+
+ for (auto& i : m_entries)
+ {
+ if (StringUtils::EqualsNoCase(host, i.host))
+ {
+ i.mac = mac;
+ ShowDiscoveryMessage(__FUNCTION__, host.c_str(), false);
+
+ AddMatchingUPnPServers(m_UPnPServers, host, mac, i.timeout);
+ SaveToXML();
+ return;
+ }
+ }
+
+ // not found entry to update - create using default values
+ WakeUpEntry entry (true);
+ entry.host = host;
+ entry.mac = mac;
+ m_entries.push_back(entry);
+ ShowDiscoveryMessage(__FUNCTION__, host.c_str(), true);
+
+ AddMatchingUPnPServers(m_UPnPServers, host, mac, entry.timeout);
+ SaveToXML();
+}
+
+void CWakeOnAccess::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ CMACDiscoveryJob* discoverJob = static_cast<CMACDiscoveryJob*>(job);
+
+ const std::string& host = discoverJob->GetHost();
+ const std::string& mac = discoverJob->GetMAC();
+
+ if (success)
+ {
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ SaveMACDiscoveryResult(host, mac);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Mac discovery failed for host '{}'", __FUNCTION__, host);
+
+ if (IsEnabled())
+ {
+ const std::string& heading = LOCALIZED(13033);
+ std::string message = StringUtils::Format(LOCALIZED(13036), host);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, message, 4000, true, 3000);
+ }
+ }
+}
+
+void CWakeOnAccess::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_POWERMANAGEMENT_WAKEONACCESS)
+ {
+ bool enabled = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+
+ SetEnabled(enabled);
+
+ if (enabled)
+ QueueMACDiscoveryForAllRemotes();
+ }
+}
+
+std::string CWakeOnAccess::GetSettingFile()
+{
+ return CSpecialProtocol::TranslatePath("special://profile/wakeonlan.xml");
+}
+
+void CWakeOnAccess::OnSettingsLoaded()
+{
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ LoadFromXML();
+}
+
+void CWakeOnAccess::SetEnabled(bool enabled)
+{
+ m_enabled = enabled;
+
+ CLog::Log(LOGINFO, "WakeOnAccess - Enabled:{}", m_enabled ? "TRUE" : "FALSE");
+}
+
+void CWakeOnAccess::LoadFromXML()
+{
+ bool enabled = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_POWERMANAGEMENT_WAKEONACCESS);
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(GetSettingFile()))
+ {
+ if (enabled)
+ CLog::Log(LOGINFO, "{} - unable to load:{}", __FUNCTION__, GetSettingFile());
+ return;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (StringUtils::CompareNoCase(pRootElement->Value(), "onaccesswakeup"))
+ {
+ CLog::Log(LOGERROR, "{} - XML file {} doesn't contain <onaccesswakeup>", __FUNCTION__,
+ GetSettingFile());
+ return;
+ }
+
+ m_entries.clear();
+
+ CLog::Log(LOGINFO, "WakeOnAccess - Load settings :");
+
+ SetEnabled(enabled);
+
+ int tmp;
+ if (XMLUtils::GetInt(pRootElement, "netinittimeout", tmp, 0, 5 * 60))
+ m_netinit_sec = tmp;
+ CLog::Log(LOGINFO, " -Network init timeout : [{}] sec", m_netinit_sec);
+
+ if (XMLUtils::GetInt(pRootElement, "netsettletime", tmp, 0, 5 * 1000))
+ m_netsettle_ms = tmp;
+ CLog::Log(LOGINFO, " -Network settle time : [{}] ms", m_netsettle_ms);
+
+ const TiXmlNode* pWakeUp = pRootElement->FirstChildElement("wakeup");
+ while (pWakeUp)
+ {
+ WakeUpEntry entry;
+
+ std::string strtmp;
+ if (XMLUtils::GetString(pWakeUp, "host", strtmp))
+ entry.host = strtmp;
+
+ if (XMLUtils::GetString(pWakeUp, "mac", strtmp))
+ entry.mac = strtmp;
+
+ if (entry.host.empty())
+ CLog::Log(LOGERROR, "{} - Missing <host> tag or it's empty", __FUNCTION__);
+ else if (entry.mac.empty())
+ CLog::Log(LOGERROR, "{} - Missing <mac> tag or it's empty", __FUNCTION__);
+ else
+ {
+ if (XMLUtils::GetInt(pWakeUp, "pingport", tmp, 0, USHRT_MAX))
+ entry.ping_port = (unsigned short) tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "pingmode", tmp, 0, USHRT_MAX))
+ entry.ping_mode = (unsigned short) tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "timeout", tmp, 10, 12 * 60 * 60))
+ entry.timeout.SetDateTimeSpan (0, 0, 0, tmp);
+
+ if (XMLUtils::GetInt(pWakeUp, "waitonline", tmp, 0, 10 * 60)) // max 10 minutes
+ entry.wait_online1_sec = tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "waitonline2", tmp, 0, 10 * 60)) // max 10 minutes
+ entry.wait_online2_sec = tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "waitservices", tmp, 0, 5 * 60)) // max 5 minutes
+ entry.wait_services_sec = tmp;
+
+ CLog::Log(LOGINFO, " Registering wakeup entry:");
+ CLog::Log(LOGINFO, " HostName : {}", entry.host);
+ CLog::Log(LOGINFO, " MacAddress : {}", entry.mac);
+ CLog::Log(LOGINFO, " PingPort : {}", entry.ping_port);
+ CLog::Log(LOGINFO, " PingMode : {}", entry.ping_mode);
+ CLog::Log(LOGINFO, " Timeout : {} (sec)", GetTotalSeconds(entry.timeout));
+ CLog::Log(LOGINFO, " WaitForOnline : {} (sec)", entry.wait_online1_sec);
+ CLog::Log(LOGINFO, " WaitForOnlineEx : {} (sec)", entry.wait_online2_sec);
+ CLog::Log(LOGINFO, " WaitForServices : {} (sec)", entry.wait_services_sec);
+
+ m_entries.push_back(entry);
+ }
+
+ pWakeUp = pWakeUp->NextSiblingElement("wakeup"); // get next one
+ }
+
+ // load upnp server map
+ m_UPnPServers.clear();
+
+ const TiXmlNode* pUPnPNode = pRootElement->FirstChildElement("upnp_map");
+ while (pUPnPNode)
+ {
+ UPnPServer server;
+
+ XMLUtils::GetString(pUPnPNode, "name", server.m_name);
+ XMLUtils::GetString(pUPnPNode, "uuid", server.m_uuid);
+ XMLUtils::GetString(pUPnPNode, "mac", server.m_mac);
+
+ if (server.m_name.empty())
+ server.m_name = server.m_uuid;
+
+ if (server.m_uuid.empty() || server.m_mac.empty())
+ CLog::Log(LOGERROR, "{} - Missing or empty <upnp_map> entry", __FUNCTION__);
+ else
+ {
+ CLog::Log(LOGINFO, " Registering upnp_map entry [{} : {}] -> [{}]", server.m_name,
+ server.m_uuid, server.m_mac);
+
+ m_UPnPServers.push_back(server);
+ }
+
+ pUPnPNode = pUPnPNode->NextSiblingElement("upnp_map"); // get next one
+ }
+}
+
+void CWakeOnAccess::SaveToXML()
+{
+ CXBMCTinyXML xmlDoc;
+ TiXmlElement xmlRootElement("onaccesswakeup");
+ TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);
+ if (!pRoot) return;
+
+ XMLUtils::SetInt(pRoot, "netinittimeout", m_netinit_sec);
+ XMLUtils::SetInt(pRoot, "netsettletime", m_netsettle_ms);
+
+ for (const auto& i : m_entries)
+ {
+ TiXmlElement xmlSetting("wakeup");
+ TiXmlNode* pWakeUpNode = pRoot->InsertEndChild(xmlSetting);
+ if (pWakeUpNode)
+ {
+ XMLUtils::SetString(pWakeUpNode, "host", i.host);
+ XMLUtils::SetString(pWakeUpNode, "mac", i.mac);
+ XMLUtils::SetInt(pWakeUpNode, "pingport", i.ping_port);
+ XMLUtils::SetInt(pWakeUpNode, "pingmode", i.ping_mode);
+ XMLUtils::SetInt(pWakeUpNode, "timeout", GetTotalSeconds(i.timeout));
+ XMLUtils::SetInt(pWakeUpNode, "waitonline", i.wait_online1_sec);
+ XMLUtils::SetInt(pWakeUpNode, "waitonline2", i.wait_online2_sec);
+ XMLUtils::SetInt(pWakeUpNode, "waitservices", i.wait_services_sec);
+ }
+ }
+
+ for (const auto& upnp : m_UPnPServers)
+ {
+ TiXmlElement xmlSetting("upnp_map");
+ TiXmlNode* pUPnPNode = pRoot->InsertEndChild(xmlSetting);
+ if (pUPnPNode)
+ {
+ XMLUtils::SetString(pUPnPNode, "name", upnp.m_name);
+ XMLUtils::SetString(pUPnPNode, "uuid", upnp.m_uuid);
+ XMLUtils::SetString(pUPnPNode, "mac", upnp.m_mac);
+ }
+ }
+
+ xmlDoc.SaveFile(GetSettingFile());
+}
diff --git a/xbmc/network/WakeOnAccess.h b/xbmc/network/WakeOnAccess.h
new file mode 100644
index 0000000..797f0c1
--- /dev/null
+++ b/xbmc/network/WakeOnAccess.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "URL.h"
+#include "XBDateTime.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/Job.h"
+
+#include <string>
+#include <vector>
+
+class CWakeOnAccess : private IJobCallback, public ISettingCallback, public ISettingsHandler
+{
+public:
+ static CWakeOnAccess &GetInstance();
+
+ bool WakeUpHost (const CURL& fileUrl);
+ bool WakeUpHost (const std::string& hostName, const std::string& customMessage);
+
+ void QueueMACDiscoveryForAllRemotes();
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingsLoaded() override;
+
+ // struct to keep per host settings
+ struct WakeUpEntry
+ {
+ explicit WakeUpEntry (bool isAwake = false);
+
+ std::string host;
+ std::string mac;
+ CDateTimeSpan timeout;
+ unsigned int wait_online1_sec; // initial wait
+ unsigned int wait_online2_sec; // extended wait
+ unsigned int wait_services_sec;
+
+ unsigned short ping_port = 0; // where to ping
+ unsigned short ping_mode = 0; // how to ping
+
+ CDateTime nextWake;
+ std::string upnpUuid; // empty unless upnpmode
+ std::string friendlyName;
+ };
+
+private:
+ CWakeOnAccess();
+ std::string GetSettingFile();
+ void LoadFromXML();
+ void SaveToXML();
+
+ void SetEnabled(bool enabled);
+ bool IsEnabled() const { return m_enabled; }
+
+ void QueueMACDiscoveryForHost(const std::string& host);
+ void SaveMACDiscoveryResult(const std::string& host, const std::string& mac);
+
+ typedef std::vector<WakeUpEntry> EntriesVector;
+ EntriesVector m_entries;
+ CCriticalSection m_entrylist_protect;
+ bool FindOrTouchHostEntry(const std::string& hostName, bool upnpMode, WakeUpEntry& server);
+ void TouchHostEntry(const std::string& hostName, bool upnpMode);
+
+ unsigned int m_netinit_sec, m_netsettle_ms; //time to wait for network connection
+
+ bool m_enabled = false;
+
+ bool WakeUpHost(const std::string& hostName, const std::string& customMessage, bool upnpMode);
+ bool WakeUpHost(const WakeUpEntry& server);
+
+ std::vector<struct UPnPServer> m_UPnPServers; // list of wakeable upnp servers
+};
diff --git a/xbmc/network/WebServer.cpp b/xbmc/network/WebServer.cpp
new file mode 100644
index 0000000..0ee1696
--- /dev/null
+++ b/xbmc/network/WebServer.cpp
@@ -0,0 +1,1409 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WebServer.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "filesystem/File.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <utility>
+
+#if defined(TARGET_POSIX)
+#include <pthread.h>
+#endif
+
+#include <inttypes.h>
+
+#define MAX_POST_BUFFER_SIZE 2048
+
+#define PAGE_FILE_NOT_FOUND \
+ "<html><head><title>File not found</title></head><body>File not found</body></html>"
+#define NOT_SUPPORTED \
+ "<html><head><title>Not Supported</title></head><body>The method you are trying to use is not " \
+ "supported by this server</body></html>"
+
+#define HEADER_VALUE_NO_CACHE "no-cache"
+
+#define HEADER_NEWLINE "\r\n"
+
+typedef struct
+{
+ std::shared_ptr<XFILE::CFile> file;
+ CHttpRanges ranges;
+ size_t rangeCountTotal;
+ std::string boundary;
+ std::string boundaryWithHeader;
+ std::string boundaryEnd;
+ bool boundaryWritten;
+ std::string contentType;
+ uint64_t writePosition;
+} HttpFileDownloadContext;
+
+CWebServer::CWebServer()
+ : m_authenticationUsername("kodi"),
+ m_authenticationPassword(""),
+ m_key(),
+ m_cert(),
+ m_logger(CServiceBroker::GetLogging().GetLogger("CWebServer"))
+{
+#if defined(TARGET_DARWIN)
+ void* stack_addr;
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_getstack(&attr, &stack_addr, &m_thread_stacksize);
+ pthread_attr_destroy(&attr);
+ // double the stack size under darwin, not sure why yet
+ // but it stopped crashing using Kodi iOS remote -> play video.
+ // non-darwin will pass a value of zero which means 'system default'
+ m_thread_stacksize *= 2;
+ m_logger->debug("increasing thread stack to {}", m_thread_stacksize);
+#endif
+}
+
+static MHD_Response* create_response(size_t size, const void* data, int free, int copy)
+{
+ MHD_ResponseMemoryMode mode = MHD_RESPMEM_PERSISTENT;
+ if (copy)
+ mode = MHD_RESPMEM_MUST_COPY;
+ else if (free)
+ mode = MHD_RESPMEM_MUST_FREE;
+ //! @bug libmicrohttpd isn't const correct
+ return MHD_create_response_from_buffer(size, const_cast<void*>(data), mode);
+}
+
+MHD_RESULT CWebServer::AskForAuthentication(const HTTPRequest& request) const
+{
+ struct MHD_Response* response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (!response)
+ {
+ m_logger->error("unable to create HTTP Unauthorized response");
+ return MHD_NO;
+ }
+
+ MHD_RESULT ret = AddHeader(response, MHD_HTTP_HEADER_CONNECTION, "close");
+ if (!ret)
+ {
+ m_logger->error("unable to prepare HTTP Unauthorized response");
+ MHD_destroy_response(response);
+ return MHD_NO;
+ }
+
+ LogResponse(request, MHD_HTTP_UNAUTHORIZED);
+
+ // This MHD_RESULT cast is only necessary for libmicrohttpd 0.9.71
+ // The return type of MHD_queue_basic_auth_fail_response was fixed for future versions
+ // See
+ // https://git.gnunet.org/libmicrohttpd.git/commit/?id=860b42e9180da4dcd7e8690a3fcdb4e37e5772c5
+ ret = static_cast<MHD_RESULT>(
+ MHD_queue_basic_auth_fail_response(request.connection, CCompileInfo::GetAppName(), response));
+ MHD_destroy_response(response);
+
+ return ret;
+}
+
+bool CWebServer::IsAuthenticated(const HTTPRequest& request) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_authenticationRequired)
+ return true;
+
+ // try to retrieve username and password for basic authentication
+ char* password = nullptr;
+ char* username = MHD_basic_auth_get_username_password(request.connection, &password);
+
+ if (username == nullptr || password == nullptr)
+ return false;
+
+ // compare the received username and password
+ bool authenticated = m_authenticationUsername.compare(username) == 0 &&
+ m_authenticationPassword.compare(password) == 0;
+
+ free(username);
+ free(password);
+
+ return authenticated;
+}
+
+MHD_RESULT CWebServer::AnswerToConnection(void* cls,
+ struct MHD_Connection* connection,
+ const char* url,
+ const char* method,
+ const char* version,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls)
+{
+ if (cls == nullptr || con_cls == nullptr || *con_cls == nullptr)
+ {
+ GetLogger()->error("invalid request received");
+ return MHD_NO;
+ }
+
+ CWebServer* webServer = reinterpret_cast<CWebServer*>(cls);
+
+ ConnectionHandler* connectionHandler = reinterpret_cast<ConnectionHandler*>(*con_cls);
+ HTTPMethod methodType = GetHTTPMethod(method);
+ HTTPRequest request = {webServer, connection, connectionHandler->fullUri, url, methodType,
+ version, {}};
+
+ if (connectionHandler->isNew)
+ webServer->LogRequest(request);
+
+ return webServer->HandlePartialRequest(connection, connectionHandler, request, upload_data,
+ upload_data_size, con_cls);
+}
+
+MHD_RESULT CWebServer::HandlePartialRequest(struct MHD_Connection* connection,
+ ConnectionHandler* connectionHandler,
+ const HTTPRequest& request,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls)
+{
+ std::unique_ptr<ConnectionHandler> conHandler(connectionHandler);
+
+ // remember if the request was new
+ bool isNewRequest = conHandler->isNew;
+ // because now it isn't anymore
+ conHandler->isNew = false;
+
+ // reset con_cls and set it if still necessary
+ *con_cls = nullptr;
+
+ if (!IsAuthenticated(request))
+ return AskForAuthentication(request);
+
+ // check if this is the first call to AnswerToConnection for this request
+ if (isNewRequest)
+ {
+ // look for a IHTTPRequestHandler which can take care of the current request
+ auto handler = FindRequestHandler(request);
+ if (handler != nullptr)
+ {
+ // if we got a GET request we need to check if it should be cached
+ if (request.method == GET || request.method == HEAD)
+ {
+ if (handler->CanBeCached())
+ {
+ bool cacheable = IsRequestCacheable(request);
+
+ CDateTime lastModified;
+ if (handler->GetLastModifiedDate(lastModified) && lastModified.IsValid())
+ {
+ // handle If-Modified-Since or If-Unmodified-Since
+ std::string ifModifiedSince = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MODIFIED_SINCE);
+ std::string ifUnmodifiedSince = HTTPRequestHandlerUtils::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)
+ {
+ struct MHD_Response* response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP 304 response");
+ return MHD_NO;
+ }
+
+ return FinalizeRequest(handler, MHD_HTTP_NOT_MODIFIED, response);
+ }
+ // handle If-Unmodified-Since
+ else if (ifUnmodifiedSinceDate.SetFromRFC1123DateTime(ifUnmodifiedSince) &&
+ lastModified.GetAsUTCDateTime() > ifUnmodifiedSinceDate)
+ return SendErrorResponse(request, MHD_HTTP_PRECONDITION_FAILED, request.method);
+ }
+
+ // pass the requested ranges on to the request handler
+ handler->SetRequestRanged(IsRequestRanged(request, lastModified));
+ }
+ }
+ // if we got a POST request we need to take care of the POST data
+ else if (request.method == POST)
+ {
+ // as ownership of the connection handler is passed to libmicrohttpd we must not destroy it
+ SetupPostDataProcessing(request, conHandler.get(), handler, con_cls);
+
+ // as ownership of the connection handler has been passed to libmicrohttpd we must not
+ // destroy it
+ conHandler.release();
+
+ return MHD_YES;
+ }
+
+ return HandleRequest(handler);
+ }
+ }
+ // this is a subsequent call to AnswerToConnection for this request
+ else
+ {
+ // again we need to take special care of the POST data
+ if (request.method == POST)
+ {
+ // process additional / remaining POST data
+ if (ProcessPostData(request, conHandler.get(), upload_data, upload_data_size, con_cls))
+ {
+ // as ownership of the connection handler has been passed to libmicrohttpd we must not
+ // destroy it
+ conHandler.release();
+
+ return MHD_YES;
+ }
+
+ // finalize POST data processing
+ FinalizePostDataProcessing(conHandler.get());
+
+ // check if something went wrong while handling the POST data
+ if (conHandler->errorStatus != MHD_HTTP_OK)
+ return SendErrorResponse(request, conHandler->errorStatus, request.method);
+
+ // we have handled all POST data so it's time to invoke the IHTTPRequestHandler
+ return HandleRequest(conHandler->requestHandler);
+ }
+
+ // it's unusual to get more than one call to AnswerToConnection for none-POST requests, but
+ // let's handle it anyway
+ auto requestHandler = FindRequestHandler(request);
+ if (requestHandler != nullptr)
+ return HandleRequest(requestHandler);
+ }
+
+ m_logger->error("couldn't find any request handler for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+}
+
+MHD_RESULT CWebServer::HandlePostField(void* cls,
+ enum MHD_ValueKind kind,
+ const char* key,
+ const char* filename,
+ const char* content_type,
+ const char* transfer_encoding,
+ const char* data,
+ uint64_t off,
+ size_t size)
+{
+ ConnectionHandler* conHandler = (ConnectionHandler*)cls;
+
+ if (conHandler == nullptr || conHandler->requestHandler == nullptr || key == nullptr ||
+ data == nullptr || size == 0)
+ {
+ GetLogger()->error("unable to handle HTTP POST field");
+ return MHD_NO;
+ }
+
+ conHandler->requestHandler->AddPostField(key, std::string(data, size));
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::HandleRequest(const std::shared_ptr<IHTTPRequestHandler>& handler)
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ HTTPRequest request = handler->GetRequest();
+ MHD_RESULT ret = handler->HandleRequest();
+ if (ret == MHD_NO)
+ {
+ m_logger->error("failed to handle HTTP request for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ struct MHD_Response* response = nullptr;
+ switch (responseDetails.type)
+ {
+ case HTTPNone:
+ m_logger->error("HTTP request handler didn't process {}", request.pathUrl);
+ return MHD_NO;
+
+ case HTTPRedirect:
+ ret = CreateRedirect(request.connection, handler->GetRedirectUrl(), response);
+ break;
+
+ case HTTPFileDownload:
+ ret = CreateFileDownloadResponse(handler, response);
+ break;
+
+ case HTTPMemoryDownloadNoFreeNoCopy:
+ case HTTPMemoryDownloadNoFreeCopy:
+ case HTTPMemoryDownloadFreeNoCopy:
+ case HTTPMemoryDownloadFreeCopy:
+ ret = CreateMemoryDownloadResponse(handler, response);
+ break;
+
+ case HTTPError:
+ ret =
+ CreateErrorResponse(request.connection, responseDetails.status, request.method, response);
+ break;
+
+ default:
+ m_logger->error("internal error while HTTP request handler processed {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ if (ret == MHD_NO)
+ {
+ m_logger->error("failed to create HTTP response for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ return FinalizeRequest(handler, responseDetails.status, response);
+}
+
+MHD_RESULT CWebServer::FinalizeRequest(const std::shared_ptr<IHTTPRequestHandler>& handler,
+ int responseStatus,
+ struct MHD_Response* response)
+{
+ if (handler == nullptr || response == nullptr)
+ 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={}", 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 all headers set by the request handler
+ for (const auto& it : responseDetails.headers)
+ AddHeader(response, it.first, it.second);
+
+ return SendResponse(request, responseStatus, response);
+}
+
+std::shared_ptr<IHTTPRequestHandler> CWebServer::FindRequestHandler(
+ const HTTPRequest& request) const
+{
+ // look for a IHTTPRequestHandler which can take care of the current request
+ auto requestHandlerIt = std::find_if(m_requestHandlers.cbegin(), m_requestHandlers.cend(),
+ [&request](const IHTTPRequestHandler* requestHandler) {
+ return requestHandler->CanHandleRequest(request);
+ });
+
+ // we found a matching IHTTPRequestHandler so let's get a new instance for this request
+ if (requestHandlerIt != m_requestHandlers.cend())
+ return std::shared_ptr<IHTTPRequestHandler>((*requestHandlerIt)->Create(request));
+
+ return nullptr;
+}
+
+bool CWebServer::IsRequestCacheable(const HTTPRequest& request) const
+{
+ // handle Cache-Control
+ std::string cacheControl = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CACHE_CONTROL);
+ if (!cacheControl.empty())
+ {
+ std::vector<std::string> cacheControls = StringUtils::Split(cacheControl, ",");
+ for (auto control : cacheControls)
+ {
+ control = StringUtils::Trim(control);
+
+ // handle no-cache
+ if (control.compare(HEADER_VALUE_NO_CACHE) == 0)
+ return false;
+ }
+ }
+
+ // handle Pragma
+ std::string pragma = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_PRAGMA);
+ if (pragma.compare(HEADER_VALUE_NO_CACHE) == 0)
+ return false;
+
+ return true;
+}
+
+bool CWebServer::IsRequestRanged(const HTTPRequest& request, const CDateTime& lastModified) const
+{
+ // parse the Range header and store it in the request object
+ CHttpRanges ranges;
+ bool ranged = ranges.Parse(HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE));
+
+ // handle If-Range header but only if the Range header is present
+ if (ranged && lastModified.IsValid())
+ {
+ std::string ifRange = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.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)
+ ranges.Clear();
+ }
+ }
+
+ return !ranges.IsEmpty();
+}
+
+void CWebServer::SetupPostDataProcessing(const HTTPRequest& request,
+ ConnectionHandler* connectionHandler,
+ std::shared_ptr<IHTTPRequestHandler> handler,
+ void** con_cls) const
+{
+ connectionHandler->requestHandler = std::move(handler);
+
+ // we might need to handle the POST data ourselves which is done in the next call to
+ // AnswerToConnection
+ *con_cls = connectionHandler;
+
+ // get the content-type of the POST data
+ const auto contentType = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
+ if (contentType.empty())
+ return;
+
+ // if the content-type is neither application/x-ww-form-urlencoded nor multipart/form-data we need
+ // to handle it ourselves
+ if (!StringUtils::EqualsNoCase(contentType, MHD_HTTP_POST_ENCODING_FORM_URLENCODED) &&
+ !StringUtils::EqualsNoCase(contentType, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA))
+ return;
+
+ // otherwise we can use MHD's POST processor
+ connectionHandler->postprocessor = MHD_create_post_processor(
+ request.connection, MAX_POST_BUFFER_SIZE, &CWebServer::HandlePostField,
+ static_cast<void*>(connectionHandler));
+
+ // MHD doesn't seem to be able to handle this post request
+ if (connectionHandler->postprocessor == nullptr)
+ {
+ m_logger->error("unable to create HTTP POST processor for {}", request.pathUrl);
+ connectionHandler->errorStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+}
+
+bool CWebServer::ProcessPostData(const HTTPRequest& request,
+ ConnectionHandler* connectionHandler,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls) const
+{
+ if (connectionHandler->requestHandler == nullptr)
+ {
+ m_logger->error("cannot handle partial HTTP POST for {} request because there is no valid "
+ "request handler available",
+ request.pathUrl);
+ connectionHandler->errorStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ // we only need to handle POST data if there actually is data left to handle
+ if (*upload_data_size == 0)
+ return false;
+
+ // we may need to handle more POST data which is done in the next call to AnswerToConnection
+ *con_cls = connectionHandler;
+
+ // if nothing has gone wrong so far, process the given POST data
+ if (connectionHandler->errorStatus == MHD_HTTP_OK)
+ {
+ bool postDataHandled = false;
+ // either use MHD's POST processor
+ if (connectionHandler->postprocessor != nullptr)
+ postDataHandled = MHD_post_process(connectionHandler->postprocessor, upload_data,
+ *upload_data_size) == MHD_YES;
+ // or simply copy the data to the handler
+ else if (connectionHandler->requestHandler != nullptr)
+ postDataHandled =
+ connectionHandler->requestHandler->AddPostData(upload_data, *upload_data_size);
+
+ // abort if the received POST data couldn't be handled
+ if (!postDataHandled)
+ {
+ m_logger->error("failed to handle HTTP POST data for {}", request.pathUrl);
+#if (MHD_VERSION >= 0x00097400)
+ connectionHandler->errorStatus = MHD_HTTP_CONTENT_TOO_LARGE;
+#elif (MHD_VERSION >= 0x00095213)
+ connectionHandler->errorStatus = MHD_HTTP_PAYLOAD_TOO_LARGE;
+#else
+ connectionHandler->errorStatus = MHD_HTTP_REQUEST_ENTITY_TOO_LARGE;
+#endif
+ }
+ }
+
+ // signal that we have handled the data
+ *upload_data_size = 0;
+
+ return true;
+}
+
+void CWebServer::FinalizePostDataProcessing(ConnectionHandler* connectionHandler) const
+{
+ if (connectionHandler->postprocessor == nullptr)
+ return;
+
+ MHD_destroy_post_processor(connectionHandler->postprocessor);
+}
+
+MHD_RESULT CWebServer::CreateMemoryDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ // check if the response is completely empty
+ if (responseRanges.empty())
+ return CreateMemoryDownloadResponse(request.connection, nullptr, 0, false, false, response);
+
+ // check if the response contains more ranges than the request asked for
+ if ((request.ranges.IsEmpty() && responseRanges.size() > 1) ||
+ (!request.ranges.IsEmpty() && responseRanges.size() > request.ranges.Size()))
+ {
+ m_logger->warn("response contains more ranges ({}) than the request asked for ({})",
+ static_cast<int>(responseRanges.size()),
+ static_cast<int>(request.ranges.Size()));
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ // if the request asked for no or only one range we can simply use MHDs memory download handler
+ // we MUST NOT send a multipart response
+ if (request.ranges.Size() <= 1)
+ {
+ CHttpResponseRange responseRange = responseRanges.front();
+ // check if the range is valid
+ if (!responseRange.IsValid())
+ {
+ m_logger->warn("invalid response data with range start at {} and end at {}",
+ responseRange.GetFirstPosition(), responseRange.GetLastPosition());
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ const void* responseData = responseRange.GetData();
+ size_t responseDataLength = static_cast<size_t>(responseRange.GetLength());
+
+ switch (responseDetails.type)
+ {
+ case HTTPMemoryDownloadNoFreeNoCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ false, false, response);
+
+ case HTTPMemoryDownloadNoFreeCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ false, true, response);
+
+ case HTTPMemoryDownloadFreeNoCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ true, false, response);
+
+ case HTTPMemoryDownloadFreeCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ true, true, response);
+
+ default:
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+ }
+
+ return CreateRangedMemoryDownloadResponse(handler, response);
+}
+
+MHD_RESULT CWebServer::CreateRangedMemoryDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ // if there's no or only one range this is not the right place
+ if (responseRanges.size() <= 1)
+ return CreateMemoryDownloadResponse(handler, response);
+
+ // extract all the valid ranges and calculate their total length
+ uint64_t firstRangePosition = 0;
+ HttpResponseRanges ranges;
+ for (const auto& range : responseRanges)
+ {
+ // ignore invalid ranges
+ if (!range.IsValid())
+ continue;
+
+ // determine the first range position
+ if (ranges.empty())
+ firstRangePosition = range.GetFirstPosition();
+
+ ranges.push_back(range);
+ }
+
+ if (ranges.empty())
+ return CreateMemoryDownloadResponse(request.connection, nullptr, 0, false, false, response);
+
+ // determine the last range position
+ uint64_t lastRangePosition = ranges.back().GetLastPosition();
+
+ // adjust the HTTP status of the response
+ handler->SetResponseStatus(MHD_HTTP_PARTIAL_CONTENT);
+ // add Content-Range header
+ handler->AddResponseHeader(
+ MHD_HTTP_HEADER_CONTENT_RANGE,
+ HttpRangeUtils::GenerateContentRangeHeaderValue(firstRangePosition, lastRangePosition,
+ responseDetails.totalLength));
+
+ // generate a multipart boundary
+ std::string multipartBoundary = HttpRangeUtils::GenerateMultipartBoundary();
+ // and the content-type
+ std::string contentType = HttpRangeUtils::GenerateMultipartBoundaryContentType(multipartBoundary);
+
+ // add Content-Type header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, contentType);
+
+ // generate the multipart boundary with the Content-Type header field
+ std::string multipartBoundaryWithHeader =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(multipartBoundary, contentType);
+
+ std::string result;
+ // add all the ranges to the result
+ for (HttpResponseRanges::const_iterator range = ranges.begin(); range != ranges.end(); ++range)
+ {
+ // add a newline before any new multipart boundary
+ if (range != ranges.begin())
+ result += HEADER_NEWLINE;
+
+ // generate and append the multipart boundary with the full header (Content-Type and
+ // Content-Length)
+ result +=
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(multipartBoundaryWithHeader, &*range);
+
+ // and append the data of the range
+ result.append(static_cast<const char*>(range->GetData()),
+ static_cast<size_t>(range->GetLength()));
+
+ // check if we need to free the range data
+ if (responseDetails.type == HTTPMemoryDownloadFreeNoCopy ||
+ responseDetails.type == HTTPMemoryDownloadFreeCopy)
+ free(const_cast<void*>(range->GetData()));
+ }
+
+ result += HttpRangeUtils::GenerateMultipartBoundaryEnd(multipartBoundary);
+
+ // add Content-Length header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_LENGTH,
+ std::to_string(static_cast<uint64_t>(result.size())));
+
+ // finally create the response
+ return CreateMemoryDownloadResponse(request.connection, result.c_str(), result.size(), false,
+ true, response);
+}
+
+MHD_RESULT CWebServer::CreateRedirect(struct MHD_Connection* connection,
+ const std::string& strURL,
+ struct MHD_Response*& response) const
+{
+ response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create HTTP redirect response to {}", strURL);
+ return MHD_NO;
+ }
+
+ AddHeader(response, MHD_HTTP_HEADER_LOCATION, strURL);
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateFileDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ std::shared_ptr<XFILE::CFile> file = std::make_shared<XFILE::CFile>();
+ std::string filePath = handler->GetResponseFile();
+
+ // access check
+ if (!CFileUtils::CheckFileAccessAllowed(filePath))
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+
+ if (!file->Open(filePath, XFILE::READ_NO_CACHE))
+ {
+ m_logger->error("Failed to open {}", filePath);
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+ }
+
+ bool ranged = false;
+ uint64_t fileLength = static_cast<uint64_t>(file->GetLength());
+
+ // get the MIME type for the Content-Type header
+ std::string mimeType = responseDetails.contentType;
+ if (mimeType.empty())
+ {
+ std::string ext = URIUtils::GetExtension(filePath);
+ StringUtils::ToLower(ext);
+ mimeType = CreateMimeTypeFromExtension(ext.c_str());
+ }
+
+ uint64_t totalLength = 0;
+ std::unique_ptr<HttpFileDownloadContext> context = std::make_unique<HttpFileDownloadContext>();
+ context->file = file;
+ context->contentType = mimeType;
+ context->boundaryWritten = false;
+ context->writePosition = 0;
+
+ if (handler->IsRequestRanged())
+ {
+ if (!request.ranges.IsEmpty())
+ context->ranges = request.ranges;
+ else
+ HTTPRequestHandlerUtils::GetRequestedRanges(request.connection, fileLength, context->ranges);
+ }
+
+ uint64_t firstPosition = 0;
+ uint64_t lastPosition = 0;
+ // if there are no ranges, add the whole range
+ if (context->ranges.IsEmpty())
+ context->ranges.Add(CHttpRange(0, fileLength - 1));
+ else
+ {
+ handler->SetResponseStatus(MHD_HTTP_PARTIAL_CONTENT);
+
+ // we need to remember that we are ranged because the range length might change and won't be
+ // reliable anymore for length comparisons
+ ranged = true;
+
+ context->ranges.GetFirstPosition(firstPosition);
+ context->ranges.GetLastPosition(lastPosition);
+ }
+
+ // remember the total number of ranges
+ context->rangeCountTotal = context->ranges.Size();
+ // remember the total length
+ totalLength = context->ranges.GetLength();
+
+ // adjust the MIME type and range length in case of multiple ranges which requires multipart
+ // boundaries
+ if (context->rangeCountTotal > 1)
+ {
+ context->boundary = HttpRangeUtils::GenerateMultipartBoundary();
+ mimeType = HttpRangeUtils::GenerateMultipartBoundaryContentType(context->boundary);
+
+ // build part of the boundary with the optional Content-Type header
+ // "--<boundary>\r\nContent-Type: <content-type>\r\n
+ context->boundaryWithHeader = HttpRangeUtils::GenerateMultipartBoundaryWithHeader(
+ context->boundary, context->contentType);
+ context->boundaryEnd = HttpRangeUtils::GenerateMultipartBoundaryEnd(context->boundary);
+
+ // 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
+ std::string completeBoundaryWithHeader =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(context->boundaryWithHeader, &*range);
+ totalLength += completeBoundaryWithHeader.size();
+
+ // add a newline before any new multipart boundary
+ if (range != context->ranges.Begin())
+ totalLength += strlen(HEADER_NEWLINE);
+ }
+ // and at the very end a special end-boundary "\r\n--<boundary>--"
+ totalLength += context->boundaryEnd.size();
+ }
+
+ // set the initial write position
+ context->ranges.GetFirstPosition(context->writePosition);
+
+ // create the response object
+ response =
+ MHD_create_response_from_callback(totalLength, 2048, &CWebServer::ContentReaderCallback,
+ context.get(), &CWebServer::ContentReaderFreeCallback);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP response for {} to be filled from{}", request.pathUrl,
+ filePath);
+ return MHD_NO;
+ }
+
+ context.release(); // ownership was passed to mhd
+
+ // add Content-Range header
+ if (ranged)
+ handler->AddResponseHeader(
+ MHD_HTTP_HEADER_CONTENT_RANGE,
+ HttpRangeUtils::GenerateContentRangeHeaderValue(firstPosition, lastPosition, fileLength));
+
+ // set the Content-Type header
+ if (!mimeType.empty())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, mimeType);
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateErrorResponse(struct MHD_Connection* connection,
+ int responseType,
+ HTTPMethod method,
+ struct MHD_Response*& response) const
+{
+ size_t payloadSize = 0;
+ const void* payload = nullptr;
+
+ switch (responseType)
+ {
+ case MHD_HTTP_NOT_FOUND:
+ payloadSize = strlen(PAGE_FILE_NOT_FOUND);
+ payload = (const void*)PAGE_FILE_NOT_FOUND;
+ break;
+
+ case MHD_HTTP_NOT_IMPLEMENTED:
+ payloadSize = strlen(NOT_SUPPORTED);
+ payload = (const void*)NOT_SUPPORTED;
+ break;
+ }
+
+ response = create_response(payloadSize, payload, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP {} error response", responseType);
+ return MHD_NO;
+ }
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateMemoryDownloadResponse(struct MHD_Connection* connection,
+ const void* data,
+ size_t size,
+ bool free,
+ bool copy,
+ struct MHD_Response*& response) const
+{
+ response = create_response(size, const_cast<void*>(data), free ? MHD_YES : MHD_NO,
+ copy ? MHD_YES : MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP download response");
+ return MHD_NO;
+ }
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::SendResponse(const HTTPRequest& request,
+ int responseStatus,
+ MHD_Response* response) const
+{
+ LogResponse(request, responseStatus);
+
+ MHD_RESULT ret = MHD_queue_response(request.connection, responseStatus, response);
+ MHD_destroy_response(response);
+
+ return ret;
+}
+
+MHD_RESULT CWebServer::SendErrorResponse(const HTTPRequest& request,
+ int errorType,
+ HTTPMethod method) const
+{
+ struct MHD_Response* response = nullptr;
+ MHD_RESULT ret = CreateErrorResponse(request.connection, errorType, method, response);
+ if (ret == MHD_NO)
+ return MHD_NO;
+
+ return SendResponse(request, errorType, response);
+}
+
+void* CWebServer::UriRequestLogger(void* cls, const char* uri)
+{
+ CWebServer* webServer = reinterpret_cast<CWebServer*>(cls);
+
+ // log the full URI
+ if (webServer == nullptr)
+ GetLogger()->debug("request received for {}", uri);
+ else
+ webServer->LogRequest(uri);
+
+ // create and return a new connection handler
+ return new ConnectionHandler(uri);
+}
+
+void CWebServer::LogRequest(const char* uri) const
+{
+ if (uri == nullptr)
+ return;
+
+ m_logger->debug("request received for {}", uri);
+}
+
+ssize_t CWebServer::ContentReaderCallback(void* cls, uint64_t pos, char* buf, size_t max)
+{
+ HttpFileDownloadContext* context = (HttpFileDownloadContext*)cls;
+ if (context == nullptr || context->file == nullptr)
+ return -1;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] write maximum {} bytes from {} ({})", max, context->writePosition,
+ pos);
+
+ // check if we need to add the end-boundary
+ if (context->rangeCountTotal > 1 && context->ranges.IsEmpty())
+ {
+ // put together the end-boundary
+ std::string endBoundary = HttpRangeUtils::GenerateMultipartBoundaryEnd(context->boundary);
+ if ((unsigned int)max != endBoundary.size())
+ return -1;
+
+ // copy the boundary into the buffer
+ memcpy(buf, endBoundary.c_str(), endBoundary.size());
+ return endBoundary.size();
+ }
+
+ CHttpRange range;
+ if (context->ranges.IsEmpty() || !context->ranges.GetFirst(range))
+ return -1;
+
+ uint64_t start = range.GetFirstPosition();
+ uint64_t end = range.GetLastPosition();
+ uint64_t maximum = (uint64_t)max;
+ int written = 0;
+
+ if (context->rangeCountTotal > 1 && !context->boundaryWritten)
+ {
+ // add a newline before any new multipart boundary
+ if (context->rangeCountTotal > context->ranges.Size())
+ {
+ size_t newlineLength = strlen(HEADER_NEWLINE);
+ memcpy(buf, HEADER_NEWLINE, newlineLength);
+ buf += newlineLength;
+ written += newlineLength;
+ maximum -= newlineLength;
+ }
+
+ // put together the boundary for the current range
+ std::string boundary =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(context->boundaryWithHeader, &range);
+
+ // copy the boundary into the buffer
+ memcpy(buf, boundary.c_str(), boundary.size());
+ // advance the buffer position
+ buf += boundary.size();
+ // update the number of written byte
+ written += boundary.size();
+ // update the maximum number of bytes
+ maximum -= boundary.size();
+ context->boundaryWritten = true;
+ }
+
+ // check if the current position is within this range
+ // if not, set it to the start position
+ if (context->writePosition < start || context->writePosition > end)
+ context->writePosition = start;
+ // adjust the maximum number of read bytes
+ maximum = std::min(maximum, end - context->writePosition + 1);
+
+ // seek to the position if necessary
+ if (context->file->GetPosition() < 0 ||
+ context->writePosition != static_cast<uint64_t>(context->file->GetPosition()))
+ context->file->Seek(context->writePosition);
+
+ // read data from the file
+ ssize_t res = context->file->Read(buf, static_cast<size_t>(maximum));
+ if (res <= 0)
+ return -1;
+
+ // add the number of read bytes to the number of written bytes
+ written += res;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] wrote {} bytes from {} in range ({} - {})", written,
+ context->writePosition, start, end);
+
+ // update the current write position
+ context->writePosition += res;
+
+ // if we have read all the data from the current range
+ // remove it from the list
+ if (context->writePosition >= end + 1)
+ {
+ context->ranges.Remove(0);
+ context->boundaryWritten = false;
+ }
+
+ return written;
+}
+
+void CWebServer::ContentReaderFreeCallback(void* cls)
+{
+ HttpFileDownloadContext* context = (HttpFileDownloadContext*)cls;
+ delete context;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] done");
+}
+
+static Logger GetMhdLogger()
+{
+ return CServiceBroker::GetLogging().GetLogger("libmicrohttpd");
+}
+
+// local helper
+static void panicHandlerForMHD(void* unused,
+ const char* file,
+ unsigned int line,
+ const char* reason)
+{
+ GetMhdLogger()->critical("serious error: reason \"{}\" in file \"{}\" at line {}",
+ reason ? reason : "", file ? file : "", line);
+ throw std::runtime_error("MHD serious error"); // FIXME: better solution?
+}
+
+// local helper
+static void logFromMHD(void* unused, const char* fmt, va_list ap)
+{
+ Logger logger = GetMhdLogger();
+ if (fmt == nullptr || fmt[0] == 0)
+ GetMhdLogger()->error("reported error with empty string");
+ else
+ {
+ std::string errDsc = StringUtils::FormatV(fmt, ap);
+ if (errDsc.empty())
+ GetMhdLogger()->error("reported error with unprintable string \"{}\"", fmt);
+ else
+ {
+ if (errDsc.at(errDsc.length() - 1) == '\n')
+ errDsc.erase(errDsc.length() - 1);
+
+ // Most common error is "aborted connection", so log it at LOGDEBUG level
+ GetMhdLogger()->debug(errDsc);
+ }
+ }
+}
+
+bool CWebServer::LoadCert(std::string& skey, std::string& scert)
+{
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+ const char* keyFile = "special://userdata/server.key";
+ const char* certFile = "special://userdata/server.pem";
+
+ if (!file.Exists(keyFile) || !file.Exists(certFile))
+ return false;
+
+ if (file.LoadFile(keyFile, buf) > 0)
+ {
+ skey.resize(buf.size());
+ skey.assign(reinterpret_cast<char*>(buf.data()));
+ file.Close();
+ }
+ else
+ m_logger->error("{}: Error loading: {}", __FUNCTION__, keyFile);
+
+ if (file.LoadFile(certFile, buf) > 0)
+ {
+ scert.resize(buf.size());
+ scert.assign(reinterpret_cast<char*>(buf.data()));
+ file.Close();
+ }
+ else
+ m_logger->error("{}: Error loading: {}", __FUNCTION__, certFile);
+
+ if (!skey.empty() && !scert.empty())
+ {
+ m_logger->info("{}: found server key: {}, certificate: {}, HTTPS support enabled", __FUNCTION__,
+ keyFile, certFile);
+ return true;
+ }
+ return false;
+}
+
+struct MHD_Daemon* CWebServer::StartMHD(unsigned int flags, int port)
+{
+ unsigned int timeout = 60 * 60 * 24;
+ const char* ciphers = "NORMAL:-VERS-TLS1.0";
+
+ MHD_set_panic_func(&panicHandlerForMHD, nullptr);
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_SERVICES_WEBSERVERSSL) &&
+ MHD_is_feature_supported(MHD_FEATURE_SSL) == MHD_YES && LoadCert(m_key, m_cert))
+ // SSL enabled
+ return MHD_start_daemon(
+ flags |
+ // one thread per connection
+ // WARNING: set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
+ // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
+ MHD_USE_THREAD_PER_CONNECTION
+#if (MHD_VERSION >= 0x00095207)
+ |
+ MHD_USE_INTERNAL_POLLING_THREAD /* MHD_USE_THREAD_PER_CONNECTION must be used only with
+ MHD_USE_INTERNAL_POLLING_THREAD since 0.9.54 */
+#endif
+ | MHD_USE_DEBUG /* Print MHD error messages to log */
+ | MHD_USE_SSL,
+ port, 0, 0, &CWebServer::AnswerToConnection, this,
+
+ MHD_OPTION_EXTERNAL_LOGGER, &logFromMHD, 0, MHD_OPTION_CONNECTION_LIMIT, 512,
+ MHD_OPTION_CONNECTION_TIMEOUT, timeout, MHD_OPTION_URI_LOG_CALLBACK,
+ &CWebServer::UriRequestLogger, this, MHD_OPTION_THREAD_STACK_SIZE, m_thread_stacksize,
+ MHD_OPTION_HTTPS_MEM_KEY, m_key.c_str(), MHD_OPTION_HTTPS_MEM_CERT, m_cert.c_str(),
+ MHD_OPTION_HTTPS_PRIORITIES, ciphers, MHD_OPTION_END);
+
+ // No SSL
+ return MHD_start_daemon(
+ flags |
+ // one thread per connection
+ // WARNING: set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
+ // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
+ MHD_USE_THREAD_PER_CONNECTION
+#if (MHD_VERSION >= 0x00095207)
+ | MHD_USE_INTERNAL_POLLING_THREAD /* MHD_USE_THREAD_PER_CONNECTION must be used only with
+ MHD_USE_INTERNAL_POLLING_THREAD since 0.9.54 */
+#endif
+ | MHD_USE_DEBUG /* Print MHD error messages to log */
+ ,
+ port, 0, 0, &CWebServer::AnswerToConnection, this,
+
+ MHD_OPTION_EXTERNAL_LOGGER, &logFromMHD, 0, MHD_OPTION_CONNECTION_LIMIT, 512,
+ MHD_OPTION_CONNECTION_TIMEOUT, timeout, MHD_OPTION_URI_LOG_CALLBACK,
+ &CWebServer::UriRequestLogger, this, MHD_OPTION_THREAD_STACK_SIZE, m_thread_stacksize,
+ MHD_OPTION_END);
+}
+
+bool CWebServer::Start(uint16_t port, const std::string& username, const std::string& password)
+{
+ SetCredentials(username, password);
+ if (!m_running)
+ {
+ // use a new logger containing the port in the name
+ m_logger = CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CWebserver[{}]", port));
+
+ int v6testSock;
+ if ((v6testSock = socket(AF_INET6, SOCK_STREAM, 0)) >= 0)
+ {
+ closesocket(v6testSock);
+ m_daemon_ip6 = StartMHD(MHD_USE_IPv6, port);
+ }
+ m_daemon_ip4 = StartMHD(0, port);
+
+ m_running = (m_daemon_ip6 != nullptr) || (m_daemon_ip4 != nullptr);
+ if (m_running)
+ {
+ m_port = port;
+ m_logger->info("Started");
+ }
+ else
+ m_logger->error("Failed to start");
+ }
+
+ return m_running;
+}
+
+bool CWebServer::Stop()
+{
+ if (!m_running)
+ return true;
+
+ if (m_daemon_ip6 != nullptr)
+ MHD_stop_daemon(m_daemon_ip6);
+
+ if (m_daemon_ip4 != nullptr)
+ MHD_stop_daemon(m_daemon_ip4);
+
+ m_running = false;
+ m_logger->info("Stopped");
+ m_port = 0;
+
+ return true;
+}
+
+bool CWebServer::IsStarted()
+{
+ return m_running;
+}
+
+bool CWebServer::WebServerSupportsSSL()
+{
+ return MHD_is_feature_supported(MHD_FEATURE_SSL) == MHD_YES;
+}
+
+void CWebServer::SetCredentials(const std::string& username, const std::string& password)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_authenticationUsername = username;
+ m_authenticationPassword = password;
+ m_authenticationRequired = !m_authenticationPassword.empty();
+}
+
+void CWebServer::RegisterRequestHandler(IHTTPRequestHandler* handler)
+{
+ if (handler == nullptr)
+ return;
+
+ const auto& it = std::find(m_requestHandlers.cbegin(), m_requestHandlers.cend(), handler);
+ if (it != m_requestHandlers.cend())
+ return;
+
+ m_requestHandlers.push_back(handler);
+ std::sort(m_requestHandlers.begin(), m_requestHandlers.end(),
+ [](IHTTPRequestHandler* lhs, IHTTPRequestHandler* rhs) {
+ return rhs->GetPriority() < lhs->GetPriority();
+ });
+}
+
+void CWebServer::UnregisterRequestHandler(IHTTPRequestHandler* handler)
+{
+ if (handler == nullptr)
+ return;
+
+ m_requestHandlers.erase(std::remove(m_requestHandlers.begin(), m_requestHandlers.end(), handler),
+ m_requestHandlers.end());
+}
+
+void CWebServer::LogRequest(const HTTPRequest& request) const
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ return;
+
+ std::multimap<std::string, std::string> headerValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND,
+ headerValues);
+ std::multimap<std::string, std::string> getValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_GET_ARGUMENT_KIND,
+ getValues);
+
+ m_logger->debug(" [IN] {} {} {}", request.version, GetHTTPMethod(request.method),
+ request.pathUrlFull);
+
+ if (!getValues.empty())
+ {
+ std::vector<std::string> values;
+ for (const auto& get : getValues)
+ values.push_back(get.first + " = " + get.second);
+
+ m_logger->debug(" [IN] Query arguments: {}", StringUtils::Join(values, "; "));
+ }
+
+ for (const auto& header : headerValues)
+ m_logger->debug(" [IN] {}: {}", header.first, header.second);
+}
+
+void CWebServer::LogResponse(const HTTPRequest& request, int responseStatus) const
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ return;
+
+ std::multimap<std::string, std::string> headerValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND,
+ headerValues);
+
+ m_logger->debug("[OUT] {} {} {}", request.version, responseStatus, request.pathUrlFull);
+
+ for (const auto& header : headerValues)
+ m_logger->debug("[OUT] {}: {}", header.first, header.second);
+}
+
+std::string CWebServer::CreateMimeTypeFromExtension(const char* ext)
+{
+ if (strcmp(ext, ".kar") == 0)
+ return "audio/midi";
+ if (strcmp(ext, ".tbn") == 0)
+ return "image/jpeg";
+
+ return CMime::GetMimeType(ext);
+}
+
+MHD_RESULT CWebServer::AddHeader(struct MHD_Response* response,
+ const std::string& name,
+ const std::string& value) const
+{
+ if (response == nullptr || name.empty())
+ return MHD_NO;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ m_logger->debug("[OUT] {}: {}", name, value);
+
+ if (name == MHD_HTTP_HEADER_CONTENT_LENGTH)
+ m_logger->warn("Attempt to override MHD automatic \"Content-Length\" header");
+
+ return MHD_add_response_header(response, name.c_str(), value.c_str());
+}
+
+Logger CWebServer::GetLogger()
+{
+ static Logger s_logger = CServiceBroker::GetLogging().GetLogger("CWebServer");
+ return s_logger;
+}
diff --git a/xbmc/network/WebServer.h b/xbmc/network/WebServer.h
new file mode 100644
index 0000000..4127524
--- /dev/null
+++ b/xbmc/network/WebServer.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/logtypes.h"
+
+#include <memory>
+#include <vector>
+
+namespace XFILE
+{
+ class CFile;
+}
+class CDateTime;
+class CVariant;
+
+class CWebServer
+{
+public:
+ CWebServer();
+ virtual ~CWebServer() = default;
+
+ bool Start(uint16_t port, const std::string &username, const std::string &password);
+ bool Stop();
+ bool IsStarted();
+ static bool WebServerSupportsSSL();
+ void SetCredentials(const std::string &username, const std::string &password);
+
+ void RegisterRequestHandler(IHTTPRequestHandler *handler);
+ void UnregisterRequestHandler(IHTTPRequestHandler *handler);
+
+protected:
+ typedef struct ConnectionHandler
+ {
+ std::string fullUri;
+ bool isNew;
+ std::shared_ptr<IHTTPRequestHandler> requestHandler;
+ struct MHD_PostProcessor *postprocessor;
+ int errorStatus;
+
+ explicit ConnectionHandler(const std::string& uri)
+ : fullUri(uri)
+ , isNew(true)
+ , requestHandler(nullptr)
+ , postprocessor(nullptr)
+ , errorStatus(MHD_HTTP_OK)
+ { }
+ } ConnectionHandler;
+
+ virtual void LogRequest(const char* uri) const;
+
+ virtual MHD_RESULT HandlePartialRequest(struct MHD_Connection *connection, ConnectionHandler* connectionHandler, const HTTPRequest& request,
+ const char *upload_data, size_t *upload_data_size, void **con_cls);
+ virtual MHD_RESULT HandleRequest(const std::shared_ptr<IHTTPRequestHandler>& handler);
+ virtual MHD_RESULT FinalizeRequest(const std::shared_ptr<IHTTPRequestHandler>& handler, int responseStatus, struct MHD_Response *response);
+
+private:
+ struct MHD_Daemon* StartMHD(unsigned int flags, int port);
+
+ std::shared_ptr<IHTTPRequestHandler> FindRequestHandler(const HTTPRequest& request) const;
+
+ MHD_RESULT AskForAuthentication(const HTTPRequest& request) const;
+ bool IsAuthenticated(const HTTPRequest& request) const;
+
+ bool IsRequestCacheable(const HTTPRequest& request) const;
+ bool IsRequestRanged(const HTTPRequest& request, const CDateTime &lastModified) const;
+
+ void SetupPostDataProcessing(const HTTPRequest& request, ConnectionHandler *connectionHandler, std::shared_ptr<IHTTPRequestHandler> handler, void **con_cls) const;
+ bool ProcessPostData(const HTTPRequest& request, ConnectionHandler *connectionHandler, const char *upload_data, size_t *upload_data_size, void **con_cls) const;
+ void FinalizePostDataProcessing(ConnectionHandler *connectionHandler) const;
+
+ MHD_RESULT CreateMemoryDownloadResponse(const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response *&response) const;
+ MHD_RESULT CreateRangedMemoryDownloadResponse(const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response *&response) const;
+
+ MHD_RESULT CreateRedirect(struct MHD_Connection *connection, const std::string &strURL, struct MHD_Response *&response) const;
+ MHD_RESULT CreateFileDownloadResponse(const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response *&response) const;
+ MHD_RESULT CreateErrorResponse(struct MHD_Connection *connection, int responseType, HTTPMethod method, struct MHD_Response *&response) const;
+ MHD_RESULT CreateMemoryDownloadResponse(struct MHD_Connection *connection, const void *data, size_t size, bool free, bool copy, struct MHD_Response *&response) const;
+
+ MHD_RESULT SendResponse(const HTTPRequest& request, int responseStatus, MHD_Response *response) const;
+ MHD_RESULT SendErrorResponse(const HTTPRequest& request, int errorType, HTTPMethod method) const;
+
+ MHD_RESULT AddHeader(struct MHD_Response *response, const std::string &name, const std::string &value) const;
+
+ void LogRequest(const HTTPRequest& request) const;
+ void LogResponse(const HTTPRequest& request, int responseStatus) const;
+
+ static std::string CreateMimeTypeFromExtension(const char *ext);
+
+ // MHD callback implementations
+ static void* UriRequestLogger(void *cls, const char *uri);
+
+ static ssize_t ContentReaderCallback (void *cls, uint64_t pos, char *buf, size_t max);
+ static void ContentReaderFreeCallback(void *cls);
+
+ static MHD_RESULT AnswerToConnection (void *cls, struct MHD_Connection *connection,
+ const char *url, const char *method,
+ const char *version, const char *upload_data,
+ size_t *upload_data_size, void **con_cls);
+ static MHD_RESULT HandlePostField(void *cls, enum MHD_ValueKind kind, const char *key,
+ const char *filename, const char *content_type,
+ const char *transfer_encoding, const char *data, uint64_t off,
+ size_t size);
+
+ bool LoadCert(std::string &skey, std::string &scert);
+
+ static Logger GetLogger();
+
+ uint16_t m_port = 0;
+ struct MHD_Daemon *m_daemon_ip6 = nullptr;
+ struct MHD_Daemon *m_daemon_ip4 = nullptr;
+ bool m_running = false;
+ size_t m_thread_stacksize = 0;
+ bool m_authenticationRequired = false;
+ std::string m_authenticationUsername;
+ std::string m_authenticationPassword;
+ std::string m_key;
+ std::string m_cert;
+ mutable CCriticalSection m_critSection;
+ std::vector<IHTTPRequestHandler *> m_requestHandlers;
+
+ Logger m_logger;
+};
diff --git a/xbmc/network/Zeroconf.cpp b/xbmc/network/Zeroconf.cpp
new file mode 100644
index 0000000..311ecfb
--- /dev/null
+++ b/xbmc/network/Zeroconf.cpp
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "Zeroconf.h"
+
+#include "ServiceBroker.h"
+
+#include <mutex>
+#if defined(HAS_MDNS)
+#include "mdns/ZeroconfMDNS.h"
+#endif
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/CriticalSection.h"
+#include "utils/JobManager.h"
+
+#if defined(TARGET_ANDROID)
+#include "platform/android/network/ZeroconfAndroid.h"
+#elif defined(TARGET_DARWIN)
+//on osx use the native implementation
+#include "platform/darwin/network/ZeroconfDarwin.h"
+#elif defined(HAS_AVAHI)
+#include "platform/linux/network/zeroconf/ZeroconfAvahi.h"
+#endif
+
+#include <cassert>
+#include <utility>
+
+namespace
+{
+
+std::mutex singletonMutex;
+
+}
+
+#ifndef HAS_ZEROCONF
+//dummy implementation used if no zeroconf is present
+//should be optimized away
+class CZeroconfDummy : public CZeroconf
+{
+ virtual bool doPublishService(const std::string&, const std::string&, const std::string&, unsigned int, const std::vector<std::pair<std::string, std::string> >&)
+ {
+ return false;
+ }
+
+ virtual bool doForceReAnnounceService(const std::string&){return false;}
+ virtual bool doRemoveService(const std::string& fcr_ident){return false;}
+ virtual void doStop(){}
+};
+#endif
+
+CZeroconf* CZeroconf::smp_instance = 0;
+
+CZeroconf::CZeroconf():mp_crit_sec(new CCriticalSection)
+{
+}
+
+CZeroconf::~CZeroconf()
+{
+ delete mp_crit_sec;
+}
+
+bool CZeroconf::PublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ std::vector<std::pair<std::string, std::string> > txt /* = std::vector<std::pair<std::string, std::string> >() */)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ CZeroconf::PublishInfo info = {fcr_type, fcr_name, f_port, std::move(txt)};
+ std::pair<tServiceMap::const_iterator, bool> ret = m_service_map.insert(std::make_pair(fcr_identifier, info));
+ if(!ret.second) //identifier exists
+ return false;
+ if(m_started)
+ CServiceBroker::GetJobManager()->AddJob(new CPublish(fcr_identifier, info), nullptr);
+
+ //not yet started, so its just queued
+ return true;
+}
+
+bool CZeroconf::RemoveService(const std::string& fcr_identifier)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ tServiceMap::iterator it = m_service_map.find(fcr_identifier);
+ if(it == m_service_map.end())
+ return false;
+ m_service_map.erase(it);
+ if(m_started)
+ return doRemoveService(fcr_identifier);
+ else
+ return true;
+}
+
+bool CZeroconf::ForceReAnnounceService(const std::string& fcr_identifier)
+{
+ if (HasService(fcr_identifier) && m_started)
+ {
+ return doForceReAnnounceService(fcr_identifier);
+ }
+ return false;
+}
+
+bool CZeroconf::HasService(const std::string& fcr_identifier) const
+{
+ return (m_service_map.find(fcr_identifier) != m_service_map.end());
+}
+
+bool CZeroconf::Start()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(!IsZCdaemonRunning())
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetBool(CSettings::SETTING_SERVICES_ZEROCONF, false);
+ if (settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ settings->SetBool(CSettings::SETTING_SERVICES_AIRPLAY, false);
+ return false;
+ }
+ if(m_started)
+ return true;
+ m_started = true;
+
+ CServiceBroker::GetJobManager()->AddJob(new CPublish(m_service_map), nullptr);
+ return true;
+}
+
+void CZeroconf::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(!m_started)
+ return;
+ doStop();
+ m_started = false;
+}
+
+CZeroconf* CZeroconf::GetInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+ if(!smp_instance)
+ {
+#ifndef HAS_ZEROCONF
+ smp_instance = new CZeroconfDummy;
+#else
+#if defined(TARGET_DARWIN)
+ smp_instance = new CZeroconfDarwin;
+#elif defined(HAS_AVAHI)
+ smp_instance = new CZeroconfAvahi;
+#elif defined(TARGET_ANDROID)
+ smp_instance = new CZeroconfAndroid;
+#elif defined(HAS_MDNS)
+ smp_instance = new CZeroconfMDNS;
+#endif
+#endif
+ }
+ assert(smp_instance);
+ return smp_instance;
+}
+
+void CZeroconf::ReleaseInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+ delete smp_instance;
+ smp_instance = 0;
+}
+
+CZeroconf::CPublish::CPublish(const std::string& fcr_identifier, const PublishInfo& pubinfo)
+{
+ m_servmap.insert(std::make_pair(fcr_identifier, pubinfo));
+}
+
+CZeroconf::CPublish::CPublish(const tServiceMap& servmap)
+ : m_servmap(servmap)
+{
+}
+
+bool CZeroconf::CPublish::DoWork()
+{
+ for (const auto& it : m_servmap)
+ CZeroconf::GetInstance()->doPublishService(it.first, it.second.type, it.second.name,
+ it.second.port, it.second.txt);
+
+ return true;
+}
diff --git a/xbmc/network/Zeroconf.h b/xbmc/network/Zeroconf.h
new file mode 100644
index 0000000..bed66c9
--- /dev/null
+++ b/xbmc/network/Zeroconf.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "utils/Job.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CCriticalSection;
+/// this class provides support for zeroconf
+/// while the different zeroconf implementations have asynchronous APIs
+/// this class hides it and provides only few ways to interact
+/// with the services. If more control is needed, feel
+/// free to add it. The main purpose currently is to provide an easy
+/// way to publish services in the different StartXXX/StopXXX methods
+/// in CApplication
+//! @todo Make me safe for use in static initialization. CritSec is a static member :/
+//! use e.g. loki's singleton implementation to make do it properly
+class CZeroconf
+{
+public:
+
+ //tries to publish this service via zeroconf
+ //fcr_identifier can be used to stop or reannounce this service later
+ //fcr_type is the zeroconf service type to publish (e.g. _http._tcp for webserver)
+ //fcr_name is the name of the service to publish. The hostname is currently automatically appended
+ // and used for name collisions. e.g. XBMC would get published as fcr_name@Martn or, after collision fcr_name@Martn-2
+ //f_port port of the service to publish
+ // returns false if fcr_identifier was already present
+ bool PublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ std::vector<std::pair<std::string, std::string> > txt /*= std::vector<std::pair<std::string, std::string> >()*/);
+
+ //tries to rebroadcast that service on the network without removing/readding
+ //this can be achieved by changing a fake txt record. Implementations should
+ //implement it by doing so.
+ //
+ //fcr_identifier - the identifier of the already published service which should be reannounced
+ // returns true on successful reannonuce - false if this service isn't published yet
+ bool ForceReAnnounceService(const std::string& fcr_identifier);
+
+ ///removes the specified service
+ ///returns false if fcr_identifier does not exist
+ bool RemoveService(const std::string& fcr_identifier);
+
+ ///returns true if fcr_identifier exists
+ bool HasService(const std::string& fcr_identifier) const;
+
+ //starts publishing
+ //services that were added with PublishService(...) while Zeroconf wasn't
+ //started, get published now.
+ bool Start();
+
+ // unpublishes all services (but keeps them stored in this class)
+ // a call to Start() will republish them
+ void Stop();
+
+ // class methods
+ // access to singleton; singleton gets created on call if not existent
+ // if zeroconf is disabled (!HAS_ZEROCONF), this will return a dummy implementation that
+ // just does nothings, otherwise the platform specific one
+ static CZeroconf* GetInstance();
+ // release the singleton; (save to call multiple times)
+ static void ReleaseInstance();
+ // returns false if ReleaseInstance() was called before
+ static bool IsInstantiated() { return smp_instance != 0; }
+ // win32: process results from the bonjour daemon
+ virtual void ProcessResults() {}
+ // returns if the service is started and services are announced
+ bool IsStarted() { return m_started; }
+
+protected:
+ //methods to implement for concrete implementations
+ //publishs this service
+ virtual bool doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt) = 0;
+
+ //methods to implement for concrete implementations
+ //update this service
+ virtual bool doForceReAnnounceService(const std::string& fcr_identifier) = 0;
+
+ //removes the service if published
+ virtual bool doRemoveService(const std::string& fcr_ident) = 0;
+
+ //removes all services (short hand for "for i in m_service_map doRemoveService(i)")
+ virtual void doStop() = 0;
+
+ // return true if the zeroconf daemon is running
+ virtual bool IsZCdaemonRunning() { return true; }
+
+protected:
+ //singleton: we don't want to get instantiated nor copied or deleted from outside
+ CZeroconf();
+ CZeroconf(const CZeroconf&);
+ virtual ~CZeroconf();
+
+private:
+ struct PublishInfo{
+ std::string type;
+ std::string name;
+ unsigned int port;
+ std::vector<std::pair<std::string, std::string> > txt;
+ };
+
+ //protects data
+ CCriticalSection* mp_crit_sec;
+ typedef std::map<std::string, PublishInfo> tServiceMap;
+ tServiceMap m_service_map;
+ bool m_started = false;
+
+ static CZeroconf* smp_instance;
+
+ class CPublish : public CJob
+ {
+ public:
+ CPublish(const std::string& fcr_identifier, const PublishInfo& pubinfo);
+ explicit CPublish(const tServiceMap& servmap);
+
+ bool DoWork() override;
+
+ private:
+ tServiceMap m_servmap;
+ };
+};
diff --git a/xbmc/network/ZeroconfBrowser.cpp b/xbmc/network/ZeroconfBrowser.cpp
new file mode 100644
index 0000000..2d1b8c6
--- /dev/null
+++ b/xbmc/network/ZeroconfBrowser.cpp
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "ZeroconfBrowser.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <mutex>
+#include <stdexcept>
+
+#if defined (HAS_AVAHI)
+#include "platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h"
+#elif defined(TARGET_DARWIN)
+//on osx use the native implementation
+#include "platform/darwin/network/ZeroconfBrowserDarwin.h"
+#elif defined(TARGET_ANDROID)
+#include "platform/android/network/ZeroconfBrowserAndroid.h"
+#elif defined(HAS_MDNS)
+#include "mdns/ZeroconfBrowserMDNS.h"
+#endif
+
+#include "threads/CriticalSection.h"
+
+namespace
+{
+
+std::mutex singletonMutex;
+
+}
+
+#if !defined(HAS_ZEROCONF)
+//dummy implementation used if no zeroconf is present
+//should be optimized away
+class CZeroconfBrowserDummy : public CZeroconfBrowser
+{
+ virtual bool doAddServiceType(const std::string&){return false;}
+ virtual bool doRemoveServiceType(const std::string&){return false;}
+ virtual std::vector<ZeroconfService> doGetFoundServices(){return std::vector<ZeroconfService>();}
+ virtual bool doResolveService(ZeroconfService&, double){return false;}
+};
+#endif
+
+CZeroconfBrowser* CZeroconfBrowser::smp_instance = 0;
+
+CZeroconfBrowser::CZeroconfBrowser():mp_crit_sec(new CCriticalSection)
+{
+#ifdef HAS_FILESYSTEM_SMB
+ AddServiceType("_smb._tcp.");
+#endif
+ AddServiceType("_ftp._tcp.");
+ AddServiceType("_webdav._tcp.");
+#ifdef HAS_FILESYSTEM_NFS
+ AddServiceType("_nfs._tcp.");
+#endif// HAS_FILESYSTEM_NFS
+}
+
+CZeroconfBrowser::~CZeroconfBrowser()
+{
+ delete mp_crit_sec;
+}
+
+void CZeroconfBrowser::Start()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(m_started)
+ return;
+ m_started = true;
+ for (const auto& it : m_services)
+ doAddServiceType(it);
+}
+
+void CZeroconfBrowser::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(!m_started)
+ return;
+ for (const auto& it : m_services)
+ RemoveServiceType(it);
+ m_started = false;
+}
+
+bool CZeroconfBrowser::AddServiceType(const std::string& fcr_service_type /*const std::string& domain*/ )
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ std::pair<tServices::iterator, bool> ret = m_services.insert(fcr_service_type);
+ if(!ret.second)
+ {
+ //service already in list
+ return false;
+ }
+ if(m_started)
+ return doAddServiceType(*ret.first);
+ //not yet started, so its just queued
+ return true;
+}
+
+bool CZeroconfBrowser::RemoveServiceType(const std::string& fcr_service_type)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ tServices::iterator ret = m_services.find(fcr_service_type);
+ if(ret == m_services.end())
+ return false;
+ if(m_started)
+ return doRemoveServiceType(fcr_service_type);
+ //not yet started, so its just queued
+ return true;
+}
+
+std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowser::GetFoundServices()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(m_started)
+ return doGetFoundServices();
+ else
+ {
+ CLog::Log(LOGDEBUG, "CZeroconfBrowser::GetFoundServices asked for services without browser running");
+ return std::vector<ZeroconfService>();
+ }
+}
+
+bool CZeroconfBrowser::ResolveService(ZeroconfService& fr_service, double f_timeout)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(m_started)
+ {
+ return doResolveService(fr_service, f_timeout);
+ }
+ CLog::Log(LOGDEBUG, "CZeroconfBrowser::GetFoundServices asked for services without browser running");
+ return false;
+}
+
+CZeroconfBrowser* CZeroconfBrowser::GetInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+
+ if(!smp_instance)
+ {
+#if !defined(HAS_ZEROCONF)
+ smp_instance = new CZeroconfBrowserDummy;
+#else
+#if defined(TARGET_DARWIN)
+ smp_instance = new CZeroconfBrowserDarwin;
+#elif defined(HAS_AVAHI)
+ smp_instance = new CZeroconfBrowserAvahi;
+#elif defined(TARGET_ANDROID)
+ // WIP
+ smp_instance = new CZeroconfBrowserAndroid;
+#elif defined(HAS_MDNS)
+ smp_instance = new CZeroconfBrowserMDNS;
+#endif
+#endif
+ }
+ assert(smp_instance);
+ return smp_instance;
+}
+
+void CZeroconfBrowser::ReleaseInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+
+ delete smp_instance;
+ smp_instance = 0;
+}
+
+
+CZeroconfBrowser::ZeroconfService::ZeroconfService(const std::string& fcr_name, const std::string& fcr_type, const std::string& fcr_domain):
+ m_name(fcr_name),
+ m_domain(fcr_domain)
+{
+ SetType(fcr_type);
+}
+void CZeroconfBrowser::ZeroconfService::SetName(const std::string& fcr_name)
+{
+ m_name = fcr_name;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetType(const std::string& fcr_type)
+{
+ if(fcr_type.empty())
+ throw std::runtime_error("CZeroconfBrowser::ZeroconfService::SetType invalid type: "+ fcr_type);
+ //make sure there's a "." as last char (differs for avahi and osx implementation of browsers)
+ if(fcr_type[fcr_type.length() - 1] != '.')
+ m_type = fcr_type + ".";
+ else
+ m_type = fcr_type;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetDomain(const std::string& fcr_domain)
+{
+ m_domain = fcr_domain;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetHostname(const std::string& fcr_hostname)
+{
+ m_hostname = fcr_hostname;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetIP(const std::string& fcr_ip)
+{
+ m_ip = fcr_ip;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetPort(int f_port)
+{
+ m_port = f_port;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetTxtRecords(const tTxtRecordMap& txt_records)
+{
+ m_txtrecords_map = txt_records;
+
+ CLog::Log(LOGDEBUG,"CZeroconfBrowser: dump txt-records");
+ for (const auto& it : m_txtrecords_map)
+ {
+ CLog::Log(LOGDEBUG, "CZeroconfBrowser: key: {} value: {}", it.first, it.second);
+ }
+}
+
+std::string CZeroconfBrowser::ZeroconfService::toPath(const ZeroconfService& fcr_service)
+{
+ return fcr_service.m_type + '@' + fcr_service.m_domain + '@' + fcr_service.m_name;
+}
+
+CZeroconfBrowser::ZeroconfService CZeroconfBrowser::ZeroconfService::fromPath(const std::string& fcr_path)
+{
+ if( fcr_path.empty() )
+ throw std::runtime_error("CZeroconfBrowser::ZeroconfService::fromPath input string empty!");
+
+ size_t pos1 = fcr_path.find('@'); //first @
+ size_t pos2 = fcr_path.find('@', pos1 + 1); //second
+
+ if(pos1 == std::string::npos || pos2 == std::string::npos)
+ throw std::runtime_error("CZeroconfBrowser::ZeroconfService::fromPath invalid input path");
+
+ return ZeroconfService(
+ fcr_path.substr(pos2 + 1, fcr_path.length()), //name
+ fcr_path.substr(0, pos1), //type
+ fcr_path.substr(pos1 + 1, pos2-(pos1+1)) //domain
+ );
+}
diff --git a/xbmc/network/ZeroconfBrowser.h b/xbmc/network/ZeroconfBrowser.h
new file mode 100644
index 0000000..76a4439
--- /dev/null
+++ b/xbmc/network/ZeroconfBrowser.h
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+#include <set>
+#include <vector>
+#include <map>
+
+#ifdef TARGET_WINDOWS
+#undef SetPort // WIN32INCLUDES this is defined as SetPortA in WinSpool.h which is being included _somewhere_
+#endif
+
+//forwards
+class CCriticalSection;
+
+/// this class provides support for zeroconf browsing
+class CZeroconfBrowser
+{
+public:
+ class ZeroconfService
+ {
+ public:
+ typedef std::map<std::string, std::string> tTxtRecordMap;
+
+ ZeroconfService() = default;
+ ZeroconfService(const std::string& fcr_name, const std::string& fcr_type, const std::string& fcr_domain);
+
+ /// easy conversion to string and back (used in czeronfdiretory to store this service)
+ ///@{
+ static std::string toPath(const ZeroconfService& fcr_service);
+ static ZeroconfService fromPath(const std::string& fcr_path); //throws std::runtime_error on failure
+ ///@}
+
+ /// general access methods
+ ///@{
+ void SetName(const std::string& fcr_name);
+ const std::string& GetName() const {return m_name;}
+
+ void SetType(const std::string& fcr_type);
+ const std::string& GetType() const {return m_type;}
+
+ void SetDomain(const std::string& fcr_domain);
+ const std::string& GetDomain() const {return m_domain;}
+ ///@}
+
+ /// access methods needed during resolve
+ ///@{
+ void SetIP(const std::string& fcr_ip);
+ const std::string& GetIP() const {return m_ip;}
+
+ void SetHostname(const std::string& fcr_hostname);
+ const std::string& GetHostname() const {return m_hostname;}
+
+ void SetPort(int f_port);
+ int GetPort() const {return m_port;}
+
+ void SetTxtRecords(const tTxtRecordMap& txt_records);
+ const tTxtRecordMap& GetTxtRecords() const { return m_txtrecords_map;}
+ ///@}
+ private:
+ //3 entries below identify a service
+ std::string m_name;
+ std::string m_type;
+ std::string m_domain;
+
+ //2 entries below store 1 ip:port pair for this service
+ std::string m_ip;
+ int m_port = 0;
+
+ //used for mdns in case dns resolution fails
+ //we store the hostname and resolve with mdns functions again
+ std::string m_hostname;
+
+ //1 entry below stores the txt-record as a key value map for this service
+ tTxtRecordMap m_txtrecords_map;
+ };
+
+ // starts browsing
+ void Start();
+
+ // stops browsing
+ void Stop();
+
+ ///returns the list of found services
+ /// if this is updated, the following message with "zeroconf://" as path is sent:
+ /// CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ std::vector<ZeroconfService> GetFoundServices();
+ ///@}
+
+ // resolves a ZeroconfService to ip + port
+ // @param fcr_service the service to resolve
+ // @param f_timeout timeout in seconds for resolving
+ // the protocol part of CURL is the raw zeroconf service type
+ // added with AddServiceType (== needs further processing! e.g. _smb._tcp -> smb)
+ // @return true if it was successfully resolved (or scheduled), false if resolve
+ // failed (async or not)
+ bool ResolveService(ZeroconfService& fr_service, double f_timeout = 1.0);
+
+ // class methods
+ // access to singleton; singleton gets created on call if not existent
+ // if zeroconf is disabled (!HAS_ZEROCONF), this will return a dummy implementation that
+ // just does nothings, otherwise the platform specific one
+ static CZeroconfBrowser* GetInstance();
+ // release the singleton; (save to call multiple times)
+ static void ReleaseInstance();
+ // returns false if ReleaseInstance() was called before
+ static bool IsInstantiated() { return smp_instance != 0; }
+
+ virtual void ProcessResults() {}
+
+ /// methods for browsing and getting results of it
+ ///@{
+ /// adds a service type for browsing
+ /// @param fcr_service_type the service type as string, e.g. _smb._tcp.
+ /// @return false if it was already there
+ bool AddServiceType(const std::string& fcr_service_type);
+
+ /// remove the specified service from discovery
+ /// @param fcr_service_type the service type as string, e.g. _smb._tcp.
+ /// @return if it was not found
+ bool RemoveServiceType(const std::string& fcr_service_type);
+
+protected:
+ //singleton: we don't want to get instantiated nor copied or deleted from outside
+ CZeroconfBrowser();
+ CZeroconfBrowser(const CZeroconfBrowser&) = delete;
+ CZeroconfBrowser& operator=(const CZeroconfBrowser&) = delete;
+ virtual ~CZeroconfBrowser();
+
+ // pure virtual methods to implement for OS specific implementations
+ virtual bool doAddServiceType(const std::string& fcr_service_type) = 0;
+ virtual bool doRemoveServiceType(const std::string& fcr_service_type) = 0;
+ virtual std::vector<ZeroconfService> doGetFoundServices() = 0;
+ virtual bool doResolveService(ZeroconfService& fr_service, double f_timeout) = 0;
+
+private:
+ struct ServiceInfo
+ {
+ std::string type;
+ };
+
+ //protects data
+ CCriticalSection* mp_crit_sec;
+ typedef std::set<std::string> tServices;
+ tServices m_services;
+ bool m_started = false;
+
+ static CZeroconfBrowser* smp_instance;
+};
+#include <iostream>
+
+//debugging helper
+inline std::ostream& operator<<(std::ostream& o, const CZeroconfBrowser::ZeroconfService& service){
+ o << "(" << service.GetName() << "|" << service.GetType() << "|" << service.GetDomain() << ")";
+ return o;
+}
+
+//inline methods
+inline bool operator<(CZeroconfBrowser::ZeroconfService const& fcr_lhs, CZeroconfBrowser::ZeroconfService const& fcr_rhs)
+{
+ return (fcr_lhs.GetName() + fcr_lhs.GetType() + fcr_lhs.GetDomain() < fcr_rhs.GetName() + fcr_rhs.GetType() + fcr_rhs.GetDomain());
+}
+
+inline bool operator==(CZeroconfBrowser::ZeroconfService const& fcr_lhs, CZeroconfBrowser::ZeroconfService const& fcr_rhs)
+{
+ return (fcr_lhs.GetName() == fcr_rhs.GetName() && fcr_lhs.GetType() == fcr_rhs.GetType() && fcr_lhs.GetDomain() == fcr_rhs.GetDomain() );
+}
diff --git a/xbmc/network/cddb.cpp b/xbmc/network/cddb.cpp
new file mode 100644
index 0000000..acbfc9f
--- /dev/null
+++ b/xbmc/network/cddb.cpp
@@ -0,0 +1,1083 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "cddb.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "network/DNSNameCache.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <taglib/id3v1genres.h>
+
+using namespace MEDIA_DETECT;
+using namespace CDDB;
+
+//-------------------------------------------------------------------------------------------------------------------
+Xcddb::Xcddb()
+#if defined(TARGET_WINDOWS)
+ : m_cddb_socket(closesocket, INVALID_SOCKET)
+#else
+ : m_cddb_socket(close, -1)
+#endif
+ , m_cddb_ip_address(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cddbAddress)
+{
+ m_lastError = 0;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+Xcddb::~Xcddb()
+{
+ closeSocket();
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::openSocket()
+{
+ char namebuf[NI_MAXHOST], portbuf[NI_MAXSERV];
+ struct addrinfo hints;
+ struct addrinfo *result, *addr;
+ char service[33];
+ int res;
+ SOCKET fd = INVALID_SOCKET;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+ sprintf(service, "%d", CDDB_PORT);
+
+ res = getaddrinfo(m_cddb_ip_address.c_str(), service, &hints, &result);
+ if(res)
+ {
+ std::string err;
+#if defined(TARGET_WINDOWS)
+ g_charsetConverter.wToUTF8(gai_strerror(res), err);
+#else
+ err = gai_strerror(res);
+#endif
+ CLog::Log(LOGERROR, "Xcddb::openSocket - failed to lookup {} with error {}", m_cddb_ip_address,
+ err);
+ res = getaddrinfo("130.179.31.49", service, &hints, &result);
+ if(res)
+ return false;
+ }
+
+ for(addr = result; addr; addr = addr->ai_next)
+ {
+ if(getnameinfo(addr->ai_addr, addr->ai_addrlen, namebuf, sizeof(namebuf), portbuf, sizeof(portbuf),NI_NUMERICHOST))
+ {
+ strcpy(namebuf, "[unknown]");
+ strcpy(portbuf, "[unknown]");
+ }
+ CLog::Log(LOGDEBUG, "Xcddb::openSocket - connecting to: {}:{} ...", namebuf, portbuf);
+
+ fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
+ if (fd == INVALID_SOCKET)
+ continue;
+
+ if (connect(fd, addr->ai_addr, addr->ai_addrlen) != SOCKET_ERROR)
+ break;
+
+ closesocket(fd);
+ fd = INVALID_SOCKET;
+ }
+
+ freeaddrinfo(result);
+ if(fd == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "Xcddb::openSocket - failed to connect to cddb");
+ return false;
+ }
+
+ m_cddb_socket.attach(fd);
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::closeSocket()
+{
+ if (m_cddb_socket)
+ {
+ m_cddb_socket.reset();
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::Send( const void *buffer, int bytes )
+{
+ std::unique_ptr<char[]> tmp_buffer(new char[bytes + 10]);
+ strcpy(tmp_buffer.get(), (const char*)buffer);
+ tmp_buffer.get()[bytes] = '.';
+ tmp_buffer.get()[bytes + 1] = 0x0d;
+ tmp_buffer.get()[bytes + 2] = 0x0a;
+ tmp_buffer.get()[bytes + 3] = 0x00;
+ int iErr = send((SOCKET)m_cddb_socket, (const char*)tmp_buffer.get(), bytes + 3, 0);
+ if (iErr <= 0)
+ {
+ return false;
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::Send( const char *buffer)
+{
+ int iErr = Send(buffer, strlen(buffer));
+ if (iErr <= 0)
+ {
+ return false;
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+std::string Xcddb::Recv(bool wait4point)
+{
+ char tmpbuffer[1];
+ char prevChar;
+ int counter = 0;
+ std::string str_buffer;
+
+
+ //##########################################################
+ // Read the buffer. Character by character
+ tmpbuffer[0]=0;
+ do
+ {
+ int lenRead;
+
+ prevChar=tmpbuffer[0];
+ lenRead = recv((SOCKET)m_cddb_socket, (char*) & tmpbuffer, 1, 0);
+
+ //Check if there was any error reading the buffer
+ if(lenRead == 0 || lenRead == SOCKET_ERROR || WSAGetLastError() == WSAECONNRESET)
+ {
+ CLog::Log(LOGERROR,
+ "Xcddb::Recv Error reading buffer. lenRead = [{}] and WSAGetLastError = [{}]",
+ lenRead, WSAGetLastError());
+ break;
+ }
+
+ //Write received data to the return string
+ str_buffer.push_back(tmpbuffer[0]);
+ counter++;
+ }while(wait4point ? prevChar != '\n' || tmpbuffer[0] != '.' : tmpbuffer[0] != '\n');
+
+
+ //##########################################################
+ // Write captured data information to the xbmc log file
+ CLog::Log(LOGDEBUG,
+ "Xcddb::Recv Captured {0} bytes // Buffer= {1} bytes. Captured data follows on next "
+ "line\n{2}",
+ counter, str_buffer.size(), str_buffer);
+
+
+ return str_buffer;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::queryCDinfo(CCdInfo* pInfo, int inexact_list_select)
+{
+ if ( pInfo == NULL )
+ {
+ m_lastError = E_PARAMETER_WRONG;
+ return false;
+ }
+
+ uint32_t discid = pInfo->GetCddbDiscId();
+
+
+ //##########################################################
+ // Compose the cddb query string
+ std::string read_buffer = getInexactCommand(inexact_list_select);
+ if (read_buffer.empty())
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Size of inexact_list_select are 0");
+ m_lastError = E_PARAMETER_WRONG;
+ return false;
+ }
+
+
+ //##########################################################
+ // Read the data from cddb
+ Recv(false); // Clear pending data on our connection
+ if (!Send(read_buffer.c_str()))
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error sending \"{}\"", read_buffer);
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select pInfo == NULL");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ std::string recv_buffer = Recv(true);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 210: //OK, CDDB database entry follows (until terminating marker)
+ // Cool, I got it ;-)
+ writeCacheFile( recv_buffer.c_str(), discid );
+ parseData(recv_buffer.c_str());
+ break;
+
+ case 401: //Specified CDDB entry not found.
+ case 402: //Server error.
+ case 403: //Database entry is corrupt.
+ case 409: //No handshake.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Quit
+ if ( ! Send("quit") )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error sending \"{}\"", "quit");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 0: //By some reason, also 0 is a valid value. This is not documented, and might depend on that no string was found and atoi then returns 0
+ case 230: //Closing connection. Goodbye.
+ break;
+
+ case 530: //error, closing connection.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Close connection
+ if ( !closeSocket() )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error closing socket");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ return true;
+}
+
+
+//-------------------------------------------------------------------------------------------------------------------
+int Xcddb::getLastError() const
+{
+ return m_lastError;
+}
+
+
+//-------------------------------------------------------------------------------------------------------------------
+const char *Xcddb::getLastErrorText() const
+{
+ switch (getLastError())
+ {
+ case E_TOC_INCORRECT:
+ return "TOC Incorrect";
+ break;
+ case E_NETWORK_ERROR_OPEN_SOCKET:
+ return "Error open Socket";
+ break;
+ case E_NETWORK_ERROR_SEND:
+ return "Error send PDU";
+ break;
+ case E_WAIT_FOR_INPUT:
+ return "Wait for Input";
+ break;
+ case E_PARAMETER_WRONG:
+ return "Error Parameter Wrong";
+ break;
+ case 202: return "No match found";
+ case 210: return "Found exact matches, list follows (until terminating marker)";
+ case 211: return "Found inexact matches, list follows (until terminating marker)";
+ case 401: return "Specified CDDB entry not found";
+ case 402: return "Server error";
+ case 403: return "Database entry is corrupt";
+ case 408: return "CGI environment error";
+ case 409: return "No handshake";
+ case 431: return "Handshake not successful, closing connection";
+ case 432: return "No connections allowed: permission denied";
+ case 433: return "No connections allowed: X users allowed, Y currently active";
+ case 434: return "No connections allowed: system load too high";
+ case 500: return "Command syntax error, command unknown, command unimplemented";
+ case 501: return "Illegal protocol level";
+ case 530: return "error, closing connection, Server error, server timeout";
+ default: return "Unknown Error";
+ }
+}
+
+
+//-------------------------------------------------------------------------------------------------------------------
+int Xcddb::cddb_sum(int n)
+{
+ int ret;
+
+ /* For backward compatibility this algorithm must not change */
+
+ ret = 0;
+
+ while (n > 0)
+ {
+ ret = ret + (n % 10);
+ n = n / 10;
+ }
+
+ return (ret);
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+uint32_t Xcddb::calc_disc_id(int tot_trks, toc cdtoc[])
+{
+ int i = 0, t = 0, n = 0;
+
+ while (i < tot_trks)
+ {
+
+ n = n + cddb_sum((cdtoc[i].min * 60) + cdtoc[i].sec);
+ i++;
+ }
+
+ t = ((cdtoc[tot_trks].min * 60) + cdtoc[tot_trks].sec) - ((cdtoc[0].min * 60) + cdtoc[0].sec);
+
+ return ((n % 0xff) << 24 | t << 8 | tot_trks);
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addTitle(const char *buffer)
+{
+ char value[2048];
+ int trk_nr = 0;
+ //TTITLEN
+ if (buffer[7] == '=')
+ { //Einstellig
+ trk_nr = buffer[6] - 47;
+ strncpy(value, buffer + 8, sizeof(value) - 1);
+ }
+ else if (buffer[8] == '=')
+ { //Zweistellig
+ trk_nr = ((buffer[6] - 48) * 10) + buffer[7] - 47;
+ strncpy(value, buffer + 9, sizeof(value) - 1);
+ }
+ else if (buffer[9] == '=')
+ { //Dreistellig
+ trk_nr = ((buffer[6] - 48) * 100) + ((buffer[7] - 48) * 10) + buffer[8] - 47;
+ strncpy(value, buffer + 10, sizeof(value) - 1);
+ }
+ else
+ {
+ return ;
+ }
+ value[sizeof(value) - 1] = '\0';
+
+ // track artist" / "track title
+ std::vector<std::string> values = StringUtils::Split(value, " / ");
+ if (values.size() > 1)
+ {
+ g_charsetConverter.unknownToUTF8(values[0]);
+ m_mapArtists[trk_nr] += values[0];
+ g_charsetConverter.unknownToUTF8(values[1]);
+ m_mapTitles[trk_nr] += values[1];
+ }
+ else if (!values.empty())
+ {
+ g_charsetConverter.unknownToUTF8(values[0]);
+ m_mapTitles[trk_nr] += values[0];
+ }
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getInexactCommand(int select) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapInexact_cddb_command_list.find(select);
+ if (i == m_mapInexact_cddb_command_list.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getInexactArtist(int select) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapInexact_artist_list.find(select);
+ if (i == m_mapInexact_artist_list.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getInexactTitle(int select) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapInexact_title_list.find(select);
+ if (i == m_mapInexact_title_list.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getTrackArtist(int track) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapArtists.find(track);
+ if (i == m_mapArtists.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getTrackTitle(int track) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapTitles.find(track);
+ if (i == m_mapTitles.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::getDiskTitle(std::string& strdisk_title) const
+{
+ strdisk_title = m_strDisk_title;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::getDiskArtist(std::string& strdisk_artist) const
+{
+ strdisk_artist = m_strDisk_artist;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::parseData(const char *buffer)
+{
+ //writeLog("parseData Start");
+
+ std::map<std::string, std::string> keywords;
+ std::list<std::string> keywordsOrder; // remember order of keywords as it appears in data received from CDDB
+
+ // Collect all the keywords and put them in map.
+ // Multiple occurrences of the same keyword indicate that
+ // the data contained on those lines should be concatenated
+ char *line;
+ const char trenner[3] = {'\n', '\r', '\0'};
+ strtok(const_cast<char*>(buffer), trenner); // skip first line
+ while ((line = strtok(0, trenner)))
+ {
+ // Lines that begin with # are comments, should be ignored
+ if (line[0] != '#')
+ {
+ char *s = strstr(line, "=");
+ if (s != NULL)
+ {
+ std::string strKeyword(line, s - line);
+ StringUtils::TrimRight(strKeyword);
+
+ std::string strValue(s+1);
+ StringUtils::Replace(strValue, "\\n", "\n");
+ StringUtils::Replace(strValue, "\\t", "\t");
+ StringUtils::Replace(strValue, "\\\\", "\\");
+
+ std::map<std::string, std::string>::const_iterator it = keywords.find(strKeyword);
+ if (it != keywords.end())
+ strValue = it->second + strValue; // keyword occurred before, concatenate
+ else
+ keywordsOrder.push_back(strKeyword);
+
+ keywords[strKeyword] = strValue;
+ }
+ }
+ }
+
+ // parse keywords
+ for (const std::string& strKeyword : keywordsOrder)
+ {
+ std::string strValue = keywords[strKeyword];
+
+ //! @todo STRING_CLEANUP
+ if (strKeyword == "DTITLE")
+ {
+ // DTITLE may contain artist and disc title, separated with " / ",
+ // for example: DTITLE=Modern Talking / Album: Victory (The 11th Album)
+ bool found = false;
+ for (int i = 0; i < (int)strValue.size() - 2; i++)
+ {
+ if (strValue[i] == ' ' && strValue[i + 1] == '/' && strValue[i + 2] == ' ')
+ {
+ m_strDisk_artist = TrimToUTF8(strValue.substr(0, i));
+ m_strDisk_title = TrimToUTF8(strValue.substr(i+3));
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ m_strDisk_title = TrimToUTF8(strValue);
+ }
+ else if (strKeyword == "DYEAR")
+ m_strYear = TrimToUTF8(strValue);
+ else if (strKeyword== "DGENRE")
+ m_strGenre = TrimToUTF8(strValue);
+ else if (StringUtils::StartsWith(strKeyword, "TTITLE"))
+ addTitle((strKeyword + "=" + strValue).c_str());
+ else if (strKeyword == "EXTD")
+ {
+ const std::string& strExtd(strValue);
+
+ if (m_strYear.empty())
+ {
+ // Extract Year from extended info
+ // as a fallback
+ size_t iPos = strExtd.find("YEAR: ");
+ if (iPos != std::string::npos) // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(strExtd.substr(iPos + 6, 4), m_strYear);
+ }
+
+ if (m_strGenre.empty())
+ {
+ // Extract ID3 Genre
+ // as a fallback
+ size_t iPos = strExtd.find("ID3G: ");
+ if (iPos != std::string::npos)
+ {
+ std::string strGenre = strExtd.substr(iPos + 5, 4);
+ StringUtils::TrimLeft(strGenre);
+ if (StringUtils::IsNaturalNumber(strGenre))
+ {
+ int iGenre = strtol(strGenre.c_str(), NULL, 10);
+ m_strGenre = TagLib::ID3v1::genre(iGenre).to8Bit(true);
+ }
+ }
+ }
+ }
+ else if (StringUtils::StartsWith(strKeyword, "EXTT"))
+ addExtended((strKeyword + "=" + strValue).c_str());
+ }
+
+ //writeLog("parseData Ende");
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addExtended(const char *buffer)
+{
+ char value[2048];
+ int trk_nr = 0;
+ //TTITLEN
+ if (buffer[5] == '=')
+ { //Einstellig
+ trk_nr = buffer[4] - 47;
+ strncpy(value, buffer + 6, sizeof(value) - 1);
+ }
+ else if (buffer[6] == '=')
+ { //Zweistellig
+ trk_nr = ((buffer[4] - 48) * 10) + buffer[5] - 47;
+ strncpy(value, buffer + 7, sizeof(value) - 1);
+ }
+ else if (buffer[7] == '=')
+ { //Dreistellig
+ trk_nr = ((buffer[4] - 48) * 100) + ((buffer[5] - 48) * 10) + buffer[6] - 47;
+ strncpy(value, buffer + 8, sizeof(value) - 1);
+ }
+ else
+ {
+ return ;
+ }
+ value[sizeof(value) - 1] = '\0';
+
+ std::string strValue;
+ std::string strValueUtf8=value;
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(strValueUtf8, strValue);
+ m_mapExtended_track[trk_nr] = strValue;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getTrackExtended(int track) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapExtended_track.find(track);
+ if (i == m_mapExtended_track.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addInexactList(const char *list)
+{
+ /*
+ 211 Found inexact matches, list follows (until terminating `.')
+ soundtrack bf0cf90f Modern Talking / Victory - The 11th Album
+ rock c90cf90f Modern Talking / Album: Victory (The 11th Album)
+ misc de0d020f Modern Talking / Ready for the victory
+ rock e00d080f Modern Talking / Album: Victory (The 11th Album)
+ rock c10d150f Modern Talking / Victory (The 11th Album)
+ .
+ */
+
+ /*
+ m_mapInexact_cddb_command_list;
+ m_mapInexact_artist_list;
+ m_mapInexact_title_list;
+ */
+ int start = 0;
+ int end = 0;
+ bool found = false;
+ int line_counter = 0;
+ // //writeLog("addInexactList Start");
+ for (unsigned int i = 0;i < strlen(list);i++)
+ {
+ if (list[i] == '\n')
+ {
+ end = i;
+ found = true;
+ }
+ if (found)
+ {
+ if (line_counter > 0)
+ {
+ addInexactListLine(line_counter, list + start, end - start - 1);
+ }
+ start = i + 1;
+ line_counter++;
+ found = false;
+ }
+ }
+ // //writeLog("addInexactList End");
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addInexactListLine(int line_cnt, const char *line, int len)
+{
+ // rock c90cf90f Modern Talking / Album: Victory (The 11th Album)
+ int search4 = 0;
+ char genre[100]; // 0
+ char discid[10]; // 1
+ char artist[1024]; // 2
+ char title[1024];
+ char cddb_command[1024];
+ int start = 0;
+ // //writeLog("addInexactListLine Start");
+ for (int i = 0;i < len;i++)
+ {
+ switch (search4)
+ {
+ case 0:
+ if (line[i] == ' ')
+ {
+ strncpy(genre, line, i);
+ genre[i] = 0x00;
+ search4 = 1;
+ start = i + 1;
+ }
+ break;
+ case 1:
+ if (line[i] == ' ')
+ {
+ strncpy(discid, line + start, i - start);
+ discid[i - start] = 0x00;
+ search4 = 2;
+ start = i + 1;
+ }
+ break;
+ case 2:
+ if (i + 2 <= len && line[i] == ' ' && line[i + 1] == '/' && line[i + 2] == ' ')
+ {
+ strncpy(artist, line + start, i - start);
+ artist[i - start] = 0x00;
+ strncpy(title, line + (i + 3), len - (i + 3));
+ title[len - (i + 3)] = 0x00;
+ }
+ break;
+ }
+ }
+ sprintf(cddb_command, "cddb read %s %s", genre, discid);
+
+ m_mapInexact_cddb_command_list[line_cnt] = cddb_command;
+
+ std::string strArtist=artist;
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(artist, strArtist);
+ m_mapInexact_artist_list[line_cnt] = strArtist;
+
+ std::string strTitle=title;
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(title, strTitle);
+ m_mapInexact_title_list[line_cnt] = strTitle;
+ // char log_string[1024];
+ // sprintf(log_string,"%u: %s - %s",line_cnt,artist,title);
+ // //writeLog(log_string);
+ // //writeLog("addInexactListLine End");
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::setCDDBIpAddress(const std::string& ip_address)
+{
+ m_cddb_ip_address = ip_address;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::setCacheDir(const std::string& pCacheDir )
+{
+ cCacheDir = pCacheDir;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::queryCache( uint32_t discid )
+{
+ if (cCacheDir.empty())
+ return false;
+
+ XFILE::CFile file;
+ if (file.Open(GetCacheFile(discid)))
+ {
+ // Got a cachehit
+ char buffer[4096];
+ file.Read(buffer, 4096);
+ file.Close();
+ parseData( buffer );
+ return true;
+ }
+
+ return false;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::writeCacheFile( const char* pBuffer, uint32_t discid )
+{
+ if (cCacheDir.empty())
+ return false;
+
+ XFILE::CFile file;
+ if (file.OpenForWrite(GetCacheFile(discid), true))
+ {
+ const bool ret = ( (size_t) file.Write((const void*)pBuffer, strlen(pBuffer) + 1) == strlen(pBuffer) + 1);
+ file.Close();
+ return ret;
+ }
+
+ return false;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::isCDCached( int nr_of_tracks, toc cdtoc[] )
+{
+ if (cCacheDir.empty())
+ return false;
+
+ return XFILE::CFile::Exists(GetCacheFile(calc_disc_id(nr_of_tracks, cdtoc)));
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getYear() const
+{
+ return m_strYear;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getGenre() const
+{
+ return m_strGenre;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::queryCDinfo(CCdInfo* pInfo)
+{
+ if ( pInfo == NULL )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo pInfo == NULL");
+ m_lastError = E_PARAMETER_WRONG;
+ return false;
+ }
+
+ int lead_out = pInfo->GetTrackCount();
+ int real_track_count = pInfo->GetTrackCount();
+ uint32_t discid = pInfo->GetCddbDiscId();
+ unsigned long frames[100];
+
+
+ //##########################################################
+ //
+ if ( queryCache(discid) )
+ {
+ CLog::Log(LOGDEBUG, "Xcddb::queryCDinfo discid [{:08x}] already cached", discid);
+ return true;
+ }
+
+ //##########################################################
+ //
+ for (int i = 0;i < lead_out;i++)
+ {
+ frames[i] = pInfo->GetTrackInformation( i + 1 ).nFrames;
+ if (i > 0 && frames[i] < frames[i - 1])
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo E_TOC_INCORRECT");
+ m_lastError = E_TOC_INCORRECT;
+ return false;
+ }
+ }
+ unsigned long complete_length = pInfo->GetDiscLength();
+
+
+ //##########################################################
+ // Open socket to cddb database
+ if ( !openSocket() )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error opening socket");
+ m_lastError = E_NETWORK_ERROR_OPEN_SOCKET;
+ return false;
+ }
+ std::string recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //OK, read/write allowed
+ case 201: //OK, read only
+ break;
+
+ case 432: //No connections allowed: permission denied
+ case 433: //No connections allowed: X users allowed, Y currently active
+ case 434: //No connections allowed: system load too high
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Send the Hello message
+ std::string version = CSysInfo::GetVersion();
+ std::string lcAppName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(lcAppName);
+ if (version.find(' ') != std::string::npos)
+ version = version.substr(0, version.find(' '));
+ std::string strGreeting = "cddb hello " + lcAppName + " kodi.tv " + CCompileInfo::GetAppName() + " " + version;
+ if ( ! Send(strGreeting.c_str()) )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", strGreeting);
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //Handshake successful
+ case 402: //Already shook hands
+ break;
+
+ case 431: //Handshake not successful, closing connection
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Set CDDB protocol-level to 5
+ if ( ! Send("proto 5"))
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", "proto 5");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //CDDB protocol level: current cur_level, supported supp_level
+ case 201: //OK, protocol version now: cur_level
+ case 502: //Protocol level already cur_level
+ break;
+
+ case 501: //Illegal protocol level.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Compose the cddb query string
+ char query_buffer[1024];
+ strcpy(query_buffer, "");
+ strcat(query_buffer, "cddb query");
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %08x", discid);
+ strcat(query_buffer, tmp_buffer);
+ }
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %i", real_track_count);
+ strcat(query_buffer, tmp_buffer);
+ }
+ for (int i = 0;i < lead_out;i++)
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %lu", frames[i]);
+ strcat(query_buffer, tmp_buffer);
+ }
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %lu", complete_length);
+ strcat(query_buffer, tmp_buffer);
+ }
+
+
+ //##########################################################
+ // Query for matches
+ if ( ! Send(query_buffer))
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", query_buffer);
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ // 200 rock d012180e Soundtrack / Hackers
+ std::string read_buffer;
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //Found exact match
+ strtok(const_cast<char *>(recv_buffer.c_str()), " ");
+ read_buffer = StringUtils::Format("cddb read {} {:08x}", strtok(NULL, " "), discid);
+ break;
+
+ case 210: //Found exact matches, list follows (until terminating marker)
+ case 211: //Found inexact matches, list follows (until terminating marker)
+ /*
+ soundtrack bf0cf90f Modern Talking / Victory - The 11th Album
+ rock c90cf90f Modern Talking / Album: Victory (The 11th Album)
+ misc de0d020f Modern Talking / Ready for the victory
+ rock e00d080f Modern Talking / Album: Victory (The 11th Album)
+ rock c10d150f Modern Talking / Victory (The 11th Album)
+ .
+ */
+ recv_buffer += Recv(true);
+ addInexactList(recv_buffer.c_str());
+ m_lastError=E_WAIT_FOR_INPUT;
+ return false; //This is actually good. The calling method will handle this
+
+ case 202: //No match found
+ CLog::Log(
+ LOGINFO,
+ "Xcddb::queryCDinfo No match found in CDDB database when doing the query shown below:\n{}",
+ query_buffer);
+ [[fallthrough]];
+ case 403: //Database entry is corrupt
+ case 409: //No handshake
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Read the data from cddb
+ if ( !Send(read_buffer.c_str()) )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", read_buffer);
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(true);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 210: //OK, CDDB database entry follows (until terminating marker)
+ // Cool, I got it ;-)
+ writeCacheFile( recv_buffer.c_str(), discid );
+ parseData(recv_buffer.c_str());
+ break;
+
+ case 401: //Specified CDDB entry not found.
+ case 402: //Server error.
+ case 403: //Database entry is corrupt.
+ case 409: //No handshake.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Quit
+ if ( ! Send("quit") )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", "quit");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 0: //By some reason, also 0 is a valid value. This is not documented, and might depend on that no string was found and atoi then returns 0
+ case 230: //Closing connection. Goodbye.
+ break;
+
+ case 530: //error, closing connection.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Close connection
+ if ( !closeSocket() )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error closing socket");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::isCDCached( CCdInfo* pInfo )
+{
+ if (cCacheDir.empty())
+ return false;
+ if ( pInfo == NULL )
+ return false;
+
+ return XFILE::CFile::Exists(GetCacheFile(pInfo->GetCddbDiscId()));
+}
+
+std::string Xcddb::GetCacheFile(uint32_t disc_id) const
+{
+ std::string strFileName;
+ strFileName = StringUtils::Format("{:x}.cddb", disc_id);
+ return URIUtils::AddFileToFolder(cCacheDir, strFileName);
+}
+
+std::string Xcddb::TrimToUTF8(const std::string &untrimmedText)
+{
+ std::string text(untrimmedText);
+ StringUtils::Trim(text);
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(text);
+ return text;
+}
diff --git a/xbmc/network/cddb.h b/xbmc/network/cddb.h
new file mode 100644
index 0000000..5217aa2
--- /dev/null
+++ b/xbmc/network/cddb.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+#include <sstream>
+#include <iostream>
+#include <map>
+#ifndef TARGET_POSIX
+#include <strstream>
+#endif
+#include "storage/cdioSupport.h"
+
+#include "utils/ScopeGuard.h"
+
+namespace CDDB
+{
+
+//Can be removed if/when removing Xcddb::queryCDinfo(int real_track_count, toc cdtoc[])
+//#define IN_PROGRESS -1
+//#define QUERY_OK 7
+//#define E_INEXACT_MATCH_FOUND 211
+//#define W_CDDB_already_shook_hands 402
+//#define E_CDDB_Handshake_not_successful 431
+
+#define E_TOC_INCORRECT 2
+#define E_NETWORK_ERROR_OPEN_SOCKET 3
+#define E_NETWORK_ERROR_SEND 4
+#define E_WAIT_FOR_INPUT 5
+#define E_PARAMETER_WRONG 6
+#define E_NO_MATCH_FOUND 202
+
+#define CDDB_PORT 8880
+
+
+struct toc
+{
+ int min;
+ int sec;
+ int frame;
+};
+
+
+class Xcddb
+{
+public:
+ Xcddb();
+ virtual ~Xcddb();
+ void setCDDBIpAddress(const std::string& ip_address);
+ void setCacheDir(const std::string& pCacheDir );
+
+// int queryCDinfo(int real_track_count, toc cdtoc[]);
+ bool queryCDinfo(MEDIA_DETECT::CCdInfo* pInfo, int inexact_list_select);
+ bool queryCDinfo(MEDIA_DETECT::CCdInfo* pInfo);
+ int getLastError() const;
+ const char * getLastErrorText() const;
+ const std::string& getYear() const;
+ const std::string& getGenre() const;
+ const std::string& getTrackArtist(int track) const;
+ const std::string& getTrackTitle(int track) const;
+ void getDiskArtist(std::string& strdisk_artist) const;
+ void getDiskTitle(std::string& strdisk_title) const;
+ const std::string& getTrackExtended(int track) const;
+ uint32_t calc_disc_id(int nr_of_tracks, toc cdtoc[]);
+ const std::string& getInexactArtist(int select) const;
+ const std::string& getInexactTitle(int select) const;
+ bool queryCache( uint32_t discid );
+ bool writeCacheFile( const char* pBuffer, uint32_t discid );
+ bool isCDCached( int nr_of_tracks, toc cdtoc[] );
+ bool isCDCached( MEDIA_DETECT::CCdInfo* pInfo );
+
+protected:
+ std::string m_strNull;
+#if defined(TARGET_WINDOWS)
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<SOCKET, INVALID_SOCKET, decltype(closesocket)>;
+#else
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<int, -1, decltype(close)>;
+#endif
+ CAutoPtrSocket m_cddb_socket;
+ const static int recv_buffer = 4096;
+ int m_lastError;
+ std::map<int, std::string> m_mapTitles;
+ std::map<int, std::string> m_mapArtists;
+ std::map<int, std::string> m_mapExtended_track;
+
+ std::map<int, std::string> m_mapInexact_cddb_command_list;
+ std::map<int, std::string> m_mapInexact_artist_list;
+ std::map<int, std::string> m_mapInexact_title_list;
+
+
+ std::string m_strDisk_artist;
+ std::string m_strDisk_title;
+ std::string m_strYear;
+ std::string m_strGenre;
+
+ void addTitle(const char *buffer);
+ void addExtended(const char *buffer);
+ void parseData(const char *buffer);
+ bool Send( const void *buffer, int bytes );
+ bool Send( const char *buffer);
+ std::string Recv(bool wait4point);
+ bool openSocket();
+ bool closeSocket();
+ struct toc cdtoc[100];
+ int cddb_sum(int n);
+ void addInexactList(const char *list);
+ void addInexactListLine(int line_cnt, const char *line, int len);
+ const std::string& getInexactCommand(int select) const;
+ std::string GetCacheFile(uint32_t disc_id) const;
+ /*! \brief Trim and convert some text to UTF8
+ \param untrimmedText original text to trim and convert
+ \return a utf8 version of the trimmed text
+ */
+ std::string TrimToUTF8(const std::string &untrimmed);
+
+ std::string m_cddb_ip_address;
+ std::string cCacheDir;
+};
+}
diff --git a/xbmc/network/dacp/CMakeLists.txt b/xbmc/network/dacp/CMakeLists.txt
new file mode 100644
index 0000000..7eb2df5
--- /dev/null
+++ b/xbmc/network/dacp/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES dacp.cpp)
+
+set(HEADERS dacp.h)
+
+core_add_library(network_dacp)
diff --git a/xbmc/network/dacp/dacp.cpp b/xbmc/network/dacp/dacp.cpp
new file mode 100644
index 0000000..890d1f2
--- /dev/null
+++ b/xbmc/network/dacp/dacp.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "dacp.h"
+
+#include "filesystem/File.h"
+
+#define AIRTUNES_DACP_CMD_URI "ctrl-int/1/"
+
+// AirTunes related DACP implementation taken from http://nto.github.io/AirPlay.html#audio-remotecontrol
+
+CDACP::CDACP(const std::string &active_remote_header, const std::string &hostname, int port)
+{
+ m_dacpUrl.SetHostName(hostname);
+ m_dacpUrl.SetPort(port);
+ m_dacpUrl.SetProtocol("http");
+ m_dacpUrl.SetProtocolOption("Active-Remote", active_remote_header);
+}
+
+void CDACP::SendCmd(const std::string &cmd)
+{
+ m_dacpUrl.SetFileName(AIRTUNES_DACP_CMD_URI + cmd);
+ // issue the command
+ XFILE::CFile file;
+ file.Open(m_dacpUrl);
+ file.Write(NULL, 0);
+}
+
+void CDACP::BeginFwd()
+{
+ SendCmd("beginff");
+}
+
+void CDACP::BeginRewnd()
+{
+ SendCmd("beginrew");
+}
+
+void CDACP::ToggleMute()
+{
+ SendCmd("mutetoggle");
+}
+
+void CDACP::NextItem()
+{
+ SendCmd("nextitem");
+}
+
+void CDACP::PrevItem()
+{
+ SendCmd("previtem");
+}
+
+void CDACP::Pause()
+{
+ SendCmd("pause");
+}
+
+void CDACP::PlayPause()
+{
+ SendCmd("playpause");
+}
+
+void CDACP::Play()
+{
+ SendCmd("play");
+}
+
+void CDACP::Stop()
+{
+ SendCmd("stop");
+}
+
+void CDACP::PlayResume()
+{
+ SendCmd("playresume");
+}
+
+void CDACP::ShuffleSongs()
+{
+ SendCmd("shuffle_songs");
+}
+
+void CDACP::VolumeDown()
+{
+ SendCmd("volumedown");
+}
+
+void CDACP::VolumeUp()
+{
+ SendCmd("volumeup");
+}
diff --git a/xbmc/network/dacp/dacp.h b/xbmc/network/dacp/dacp.h
new file mode 100644
index 0000000..074b477
--- /dev/null
+++ b/xbmc/network/dacp/dacp.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "URL.h"
+
+#include <string>
+
+class CDACP
+{
+ public:
+ CDACP(const std::string &active_remote_header, const std::string &hostname, int port);
+
+ void BeginFwd();
+ void BeginRewnd();
+ void ToggleMute();
+ void NextItem();
+ void PrevItem();
+ void Pause();
+ void PlayPause();
+ void Play();
+ void Stop();
+ void PlayResume();
+ void ShuffleSongs();
+ void VolumeDown();
+ void VolumeUp();
+
+ private:
+ void SendCmd(const std::string &cmd);
+
+ CURL m_dacpUrl;
+};
diff --git a/xbmc/network/httprequesthandler/CMakeLists.txt b/xbmc/network/httprequesthandler/CMakeLists.txt
new file mode 100644
index 0000000..ea514c5
--- /dev/null
+++ b/xbmc/network/httprequesthandler/CMakeLists.txt
@@ -0,0 +1,30 @@
+if(MICROHTTPD_FOUND)
+ set(SOURCES HTTPFileHandler.cpp
+ HTTPImageHandler.cpp
+ HTTPImageTransformationHandler.cpp
+ HTTPJsonRpcHandler.cpp
+ HTTPRequestHandlerUtils.cpp
+ HTTPVfsHandler.cpp
+ HTTPWebinterfaceAddonsHandler.cpp
+ HTTPWebinterfaceHandler.cpp
+ IHTTPRequestHandler.cpp)
+
+ if(PYTHON_FOUND)
+ list(APPEND SOURCES HTTPPythonHandler.cpp)
+ endif()
+
+ set(HEADERS HTTPFileHandler.h
+ HTTPImageHandler.h
+ HTTPImageTransformationHandler.h
+ HTTPJsonRpcHandler.h
+ HTTPRequestHandlerUtils.h
+ HTTPVfsHandler.h
+ HTTPWebinterfaceAddonsHandler.h
+ HTTPWebinterfaceHandler.h
+ IHTTPRequestHandler.h)
+ if(PYTHON_FOUND)
+ list(APPEND HEADERS HTTPPythonHandler.h)
+ endif()
+
+ core_add_library(network_httprequesthandlers)
+endif()
diff --git a/xbmc/network/httprequesthandler/HTTPFileHandler.cpp b/xbmc/network/httprequesthandler/HTTPFileHandler.cpp
new file mode 100644
index 0000000..466ab9f
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPFileHandler.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPFileHandler.h"
+
+#include "filesystem/File.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+CHTTPFileHandler::CHTTPFileHandler()
+ : IHTTPRequestHandler(),
+ m_url(),
+ m_lastModified()
+{ }
+
+CHTTPFileHandler::CHTTPFileHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request),
+ m_url(),
+ m_lastModified()
+{ }
+
+MHD_RESULT CHTTPFileHandler::HandleRequest()
+{
+ return !m_url.empty() ? MHD_YES : MHD_NO;
+}
+
+bool CHTTPFileHandler::GetLastModifiedDate(CDateTime &lastModified) const
+{
+ if (!m_lastModified.IsValid())
+ return false;
+
+ lastModified = m_lastModified;
+ return true;
+}
+
+void CHTTPFileHandler::SetFile(const std::string& file, int responseStatus)
+{
+ m_url = file;
+ m_response.status = responseStatus;
+ if (m_url.empty())
+ return;
+
+ // translate the response status into the response type
+ if (m_response.status == MHD_HTTP_OK)
+ m_response.type = HTTPFileDownload;
+ else if (m_response.status == MHD_HTTP_FOUND)
+ m_response.type = HTTPRedirect;
+ else
+ m_response.type = HTTPError;
+
+ // try to determine some additional information if the file can be downloaded
+ if (m_response.type == HTTPFileDownload)
+ {
+ // determine the content type
+ std::string ext = URIUtils::GetExtension(m_url);
+ StringUtils::ToLower(ext);
+ m_response.contentType = CMime::GetMimeType(ext);
+
+ // determine the last modified date
+ XFILE::CFile fileObj;
+ if (!fileObj.Open(m_url, XFILE::READ_NO_CACHE))
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+ else
+ {
+ struct __stat64 statBuffer;
+ if (fileObj.Stat(&statBuffer) == 0)
+ SetLastModifiedDate(&statBuffer);
+ }
+ }
+
+ // disable ranges and caching if the file can't be downloaded
+ if (m_response.type != HTTPFileDownload)
+ {
+ m_canHandleRanges = false;
+ m_canBeCached = false;
+ }
+
+ // disable caching if the last modified date couldn't be read
+ if (!m_lastModified.IsValid())
+ m_canBeCached = false;
+}
+
+void CHTTPFileHandler::SetLastModifiedDate(const struct __stat64 *statBuffer)
+{
+ struct tm *time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((const time_t*)&statBuffer->st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer->st_mtime);
+#endif
+ if (time != NULL)
+ m_lastModified = *time;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPFileHandler.h b/xbmc/network/httprequesthandler/HTTPFileHandler.h
new file mode 100644
index 0000000..1562977
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPFileHandler.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <string>
+
+class CHTTPFileHandler : public IHTTPRequestHandler
+{
+public:
+ ~CHTTPFileHandler() override = default;
+
+ MHD_RESULT HandleRequest() override;
+
+ bool CanHandleRanges() const override { return m_canHandleRanges; }
+ bool CanBeCached() const override { return m_canBeCached; }
+ bool GetLastModifiedDate(CDateTime &lastModified) const override;
+
+ std::string GetRedirectUrl() const override { return m_url; }
+ std::string GetResponseFile() const override { return m_url; }
+
+protected:
+ CHTTPFileHandler();
+ explicit CHTTPFileHandler(const HTTPRequest &request);
+
+ void SetFile(const std::string& file, int responseStatus);
+
+ void SetCanHandleRanges(bool canHandleRanges) { m_canHandleRanges = canHandleRanges; }
+ void SetCanBeCached(bool canBeCached) { m_canBeCached = canBeCached; }
+ void SetLastModifiedDate(const struct __stat64 *buffer);
+
+private:
+ std::string m_url;
+
+ bool m_canHandleRanges = true;
+ bool m_canBeCached = true;
+
+ CDateTime m_lastModified;
+
+};
diff --git a/xbmc/network/httprequesthandler/HTTPImageHandler.cpp b/xbmc/network/httprequesthandler/HTTPImageHandler.cpp
new file mode 100644
index 0000000..6cde13b
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageHandler.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPImageHandler.h"
+
+#include "URL.h"
+#include "filesystem/ImageFile.h"
+#include "network/WebServer.h"
+#include "utils/FileUtils.h"
+
+
+CHTTPImageHandler::CHTTPImageHandler(const HTTPRequest &request)
+ : CHTTPFileHandler(request)
+{
+ std::string file;
+ int responseStatus = MHD_HTTP_BAD_REQUEST;
+
+ // resolve the URL into a file path and a HTTP response status
+ if (m_request.pathUrl.size() > 7)
+ {
+ file = m_request.pathUrl.substr(7);
+
+ XFILE::CImageFile imageFile;
+ const CURL pathToUrl(file);
+ if (imageFile.Exists(pathToUrl) && CFileUtils::CheckFileAccessAllowed(file))
+ {
+ responseStatus = MHD_HTTP_OK;
+ struct __stat64 statBuffer;
+ if (imageFile.Stat(pathToUrl, &statBuffer) == 0)
+ {
+ SetLastModifiedDate(&statBuffer);
+ SetCanBeCached(true);
+ }
+ }
+ else
+ responseStatus = MHD_HTTP_NOT_FOUND;
+ }
+
+ // set the file and the HTTP response status
+ SetFile(file, responseStatus);
+}
+
+bool CHTTPImageHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return request.pathUrl.find("/image/") == 0;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPImageHandler.h b/xbmc/network/httprequesthandler/HTTPImageHandler.h
new file mode 100644
index 0000000..97df097
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageHandler.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/httprequesthandler/HTTPFileHandler.h"
+
+#include <string>
+
+class CHTTPImageHandler : public CHTTPFileHandler
+{
+public:
+ CHTTPImageHandler() = default;
+ ~CHTTPImageHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPImageHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ int GetPriority() const override { return 5; }
+ int GetMaximumAgeForCaching() const override { return 60 * 60 * 24 * 7; }
+
+protected:
+ explicit CHTTPImageHandler(const HTTPRequest &request);
+};
diff --git a/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp
new file mode 100644
index 0000000..3d2dccb
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPImageTransformationHandler.h"
+
+#include "TextureCacheJob.h"
+#include "URL.h"
+#include "filesystem/ImageFile.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <map>
+
+#define TRANSFORMATION_OPTION_WIDTH "width"
+#define TRANSFORMATION_OPTION_HEIGHT "height"
+#define TRANSFORMATION_OPTION_SCALING_ALGORITHM "scaling_algorithm"
+
+static const std::string ImageBasePath = "/image/";
+
+CHTTPImageTransformationHandler::CHTTPImageTransformationHandler()
+ : m_url(),
+ m_lastModified(),
+ m_buffer(NULL),
+ m_responseData()
+{ }
+
+CHTTPImageTransformationHandler::CHTTPImageTransformationHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request),
+ m_url(),
+ m_lastModified(),
+ m_buffer(NULL),
+ m_responseData()
+{
+ m_url = m_request.pathUrl.substr(ImageBasePath.size());
+ if (m_url.empty())
+ {
+ m_response.status = MHD_HTTP_BAD_REQUEST;
+ m_response.type = HTTPError;
+ return;
+ }
+
+ XFILE::CImageFile imageFile;
+ const CURL pathToUrl(m_url);
+ if (!imageFile.Exists(pathToUrl))
+ {
+ m_response.status = MHD_HTTP_NOT_FOUND;
+ m_response.type = HTTPError;
+ return;
+ }
+
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.status = MHD_HTTP_OK;
+
+ // determine the content type
+ std::string ext = URIUtils::GetExtension(pathToUrl.GetHostName());
+ StringUtils::ToLower(ext);
+ m_response.contentType = CMime::GetMimeType(ext);
+
+ //! @todo determine the maximum age
+
+ // determine the last modified date
+ struct __stat64 statBuffer;
+ if (imageFile.Stat(pathToUrl, &statBuffer) != 0)
+ return;
+
+ struct tm *time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer.st_mtime);
+#endif
+ if (time == NULL)
+ return;
+
+ m_lastModified = *time;
+}
+
+CHTTPImageTransformationHandler::~CHTTPImageTransformationHandler()
+{
+ m_responseData.clear();
+ delete m_buffer;
+ m_buffer = NULL;
+}
+
+bool CHTTPImageTransformationHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ if ((request.method != GET && request.method != HEAD) ||
+ request.pathUrl.find(ImageBasePath) != 0 || request.pathUrl.size() <= ImageBasePath.size())
+ return false;
+
+ // get the transformation options
+ std::map<std::string, std::string> options;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_GET_ARGUMENT_KIND, options);
+
+ return (options.find(TRANSFORMATION_OPTION_WIDTH) != options.end() ||
+ options.find(TRANSFORMATION_OPTION_HEIGHT) != options.end());
+}
+
+MHD_RESULT CHTTPImageTransformationHandler::HandleRequest()
+{
+ if (m_response.type == HTTPError)
+ return MHD_YES;
+
+ // get the transformation options
+ std::map<std::string, std::string> options;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, options);
+
+ std::vector<std::string> urlOptions;
+ std::map<std::string, std::string>::const_iterator option = options.find(TRANSFORMATION_OPTION_WIDTH);
+ if (option != options.end())
+ urlOptions.push_back(TRANSFORMATION_OPTION_WIDTH "=" + option->second);
+
+ option = options.find(TRANSFORMATION_OPTION_HEIGHT);
+ if (option != options.end())
+ urlOptions.push_back(TRANSFORMATION_OPTION_HEIGHT "=" + option->second);
+
+ option = options.find(TRANSFORMATION_OPTION_SCALING_ALGORITHM);
+ if (option != options.end())
+ urlOptions.push_back(TRANSFORMATION_OPTION_SCALING_ALGORITHM "=" + option->second);
+
+ std::string imagePath = m_url;
+ if (!urlOptions.empty())
+ {
+ imagePath += "?";
+ imagePath += StringUtils::Join(urlOptions, "&");
+ }
+
+ // resize the image into the local buffer
+ size_t bufferSize;
+ if (!CTextureCacheJob::ResizeTexture(imagePath, m_buffer, bufferSize))
+ {
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.type = HTTPError;
+
+ return MHD_YES;
+ }
+
+ // store the size of the image
+ m_response.totalLength = bufferSize;
+
+ // nothing else to do if the request is not ranged
+ if (!GetRequestedRanges(m_response.totalLength))
+ {
+ m_responseData.push_back(CHttpResponseRange(m_buffer, 0, m_response.totalLength - 1));
+ return MHD_YES;
+ }
+
+ for (HttpRanges::const_iterator range = m_request.ranges.Begin(); range != m_request.ranges.End(); ++range)
+ m_responseData.push_back(CHttpResponseRange(m_buffer + range->GetFirstPosition(), range->GetFirstPosition(), range->GetLastPosition()));
+
+ return MHD_YES;
+}
+
+bool CHTTPImageTransformationHandler::GetLastModifiedDate(CDateTime &lastModified) const
+{
+ if (!m_lastModified.IsValid())
+ return false;
+
+ lastModified = m_lastModified;
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h
new file mode 100644
index 0000000..6d8732d
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <stdint.h>
+#include <string>
+
+class CHTTPImageTransformationHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPImageTransformationHandler();
+ ~CHTTPImageTransformationHandler() override;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPImageTransformationHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request)const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ bool CanHandleRanges() const override { return true; }
+ bool CanBeCached() const override { return true; }
+ bool GetLastModifiedDate(CDateTime &lastModified) const override;
+
+ HttpResponseRanges GetResponseData() const override { return m_responseData; }
+
+ // priority must be higher than the one of CHTTPImageHandler
+ int GetPriority() const override { return 6; }
+
+protected:
+ explicit CHTTPImageTransformationHandler(const HTTPRequest &request);
+
+private:
+ std::string m_url;
+ CDateTime m_lastModified;
+
+ uint8_t* m_buffer;
+ HttpResponseRanges m_responseData;
+};
diff --git a/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp
new file mode 100644
index 0000000..9cfdbc5
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPJsonRpcHandler.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "interfaces/json-rpc/JSONServiceDescription.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "utils/FileUtils.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#define MAX_HTTP_POST_SIZE 65536
+
+bool CHTTPJsonRpcHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return (request.pathUrl.compare("/jsonrpc") == 0);
+}
+
+MHD_RESULT CHTTPJsonRpcHandler::HandleRequest()
+{
+ CHTTPClient client(m_request.method);
+ bool isRequest = false;
+ std::string jsonpCallback;
+
+ // get all query arguments
+ std::map<std::string, std::string> arguments;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, arguments);
+
+ if (m_request.method == POST)
+ {
+ std::string contentType = HTTPRequestHandlerUtils::GetRequestHeaderValue(m_request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
+ // If the content-type of the m_request was specified, it must be application/json-rpc, application/json, or application/jsonrequest
+ // http://www.jsonrpc.org/historical/json-rpc-over-http.html
+ if (!contentType.empty() && contentType.compare("application/json-rpc") != 0 &&
+ contentType.compare("application/json") != 0 && contentType.compare("application/jsonrequest") != 0)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_UNSUPPORTED_MEDIA_TYPE;
+ return MHD_YES;
+ }
+
+ isRequest = true;
+ }
+ else if (m_request.method == GET || m_request.method == HEAD)
+ {
+ std::map<std::string, std::string>::const_iterator argument = arguments.find("request");
+ if (argument != arguments.end() && !argument->second.empty())
+ {
+ m_requestData = argument->second;
+ isRequest = true;
+ }
+ }
+
+ std::map<std::string, std::string>::const_iterator argument = arguments.find("jsonp");
+ if (argument != arguments.end() && !argument->second.empty())
+ jsonpCallback = argument->second;
+ else
+ {
+ argument = arguments.find("callback");
+ if (argument != arguments.end() && !argument->second.empty())
+ jsonpCallback = argument->second;
+ }
+
+ if (isRequest)
+ {
+ m_responseData = JSONRPC::CJSONRPC::MethodCall(m_requestData, &m_transportLayer, &client);
+
+ if (!jsonpCallback.empty())
+ m_responseData = jsonpCallback + "(" + m_responseData + ");";
+ }
+ else if (jsonpCallback.empty())
+ {
+ // get the whole output of JSONRPC.Introspect
+ CVariant result;
+ JSONRPC::CJSONServiceDescription::Print(result, &m_transportLayer, &client);
+ if (!CJSONVariantWriter::Write(result, m_responseData, false))
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ return MHD_YES;
+ }
+ }
+ else
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_BAD_REQUEST;
+
+ return MHD_YES;
+ }
+
+ m_requestData.clear();
+
+ m_responseRange.SetData(m_responseData.c_str(), m_responseData.size());
+
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.status = MHD_HTTP_OK;
+ m_response.contentType = "application/json";
+ m_response.totalLength = m_responseData.size();
+
+ return MHD_YES;
+}
+
+HttpResponseRanges CHTTPJsonRpcHandler::GetResponseData() const
+{
+ HttpResponseRanges ranges;
+ ranges.push_back(m_responseRange);
+
+ return ranges;
+}
+
+bool CHTTPJsonRpcHandler::appendPostData(const char *data, size_t size)
+{
+ if (m_requestData.size() + size > MAX_HTTP_POST_SIZE)
+ {
+ CServiceBroker::GetLogging()
+ .GetLogger("CHTTPJsonRpcHandler")
+ ->error("Stopped uploading POST data since it exceeded size limitations ({})",
+ MAX_HTTP_POST_SIZE);
+ return false;
+ }
+
+ m_requestData.append(data, size);
+
+ return true;
+}
+
+bool CHTTPJsonRpcHandler::CHTTPTransportLayer::PrepareDownload(const char *path, CVariant &details, std::string &protocol)
+{
+ if (!CFileUtils::Exists(path))
+ return false;
+
+ protocol = "http";
+ std::string url;
+ std::string strPath = path;
+ if (StringUtils::StartsWith(strPath, "image://") ||
+ (StringUtils::StartsWith(strPath, "special://") && StringUtils::EndsWith(strPath, ".tbn")))
+ url = "image/";
+ else
+ url = "vfs/";
+ url += CURL::Encode(strPath);
+ details["path"] = url;
+
+ return true;
+}
+
+bool CHTTPJsonRpcHandler::CHTTPTransportLayer::Download(const char *path, CVariant &result)
+{
+ return false;
+}
+
+int CHTTPJsonRpcHandler::CHTTPTransportLayer::GetCapabilities()
+{
+ return JSONRPC::Response | JSONRPC::FileDownloadRedirect;
+}
+
+CHTTPJsonRpcHandler::CHTTPClient::CHTTPClient(HTTPMethod method)
+ : m_permissionFlags(JSONRPC::ReadData)
+{
+ // with a HTTP POST request everything is allowed
+ if (method == POST)
+ m_permissionFlags = JSONRPC::OPERATION_PERMISSION_ALL;
+}
+
+int CHTTPJsonRpcHandler::CHTTPClient::GetAnnouncementFlags()
+{
+ // Does not support broadcast
+ return 0;
+}
+
+bool CHTTPJsonRpcHandler::CHTTPClient::SetAnnouncementFlags(int flags)
+{
+ return false;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h
new file mode 100644
index 0000000..88d4496
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/json-rpc/IClient.h"
+#include "interfaces/json-rpc/ITransportLayer.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <string>
+
+class CHTTPJsonRpcHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPJsonRpcHandler() = default;
+ ~CHTTPJsonRpcHandler() override = default;
+
+ // implementations of IHTTPRequestHandler
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPJsonRpcHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ HttpResponseRanges GetResponseData() const override;
+
+ int GetPriority() const override { return 5; }
+
+protected:
+ explicit CHTTPJsonRpcHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request)
+ { }
+
+ bool appendPostData(const char *data, size_t size) override;
+
+private:
+ std::string m_requestData;
+ std::string m_responseData;
+ CHttpResponseRange m_responseRange;
+
+ class CHTTPTransportLayer : public JSONRPC::ITransportLayer
+ {
+ public:
+ CHTTPTransportLayer() = default;
+ ~CHTTPTransportLayer() override = default;
+
+ // implementations of JSONRPC::ITransportLayer
+ bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) override;
+ bool Download(const char *path, CVariant &result) override;
+ int GetCapabilities() override;
+ };
+ CHTTPTransportLayer m_transportLayer;
+
+ class CHTTPClient : public JSONRPC::IClient
+ {
+ public:
+ explicit CHTTPClient(HTTPMethod method);
+ ~CHTTPClient() override = default;
+
+ int GetPermissionFlags() override { return m_permissionFlags; }
+ int GetAnnouncementFlags() override;
+ bool SetAnnouncementFlags(int flags) override;
+
+ private:
+ int m_permissionFlags;
+ };
+};
diff --git a/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp b/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp
new file mode 100644
index 0000000..8633744
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPPythonHandler.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/File.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "interfaces/python/XBPython.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "network/httprequesthandler/HTTPWebinterfaceHandler.h"
+#include "network/httprequesthandler/python/HTTPPythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonWsgiInvoker.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#define MAX_STRING_POST_SIZE 20000
+
+CHTTPPythonHandler::CHTTPPythonHandler()
+ : IHTTPRequestHandler(),
+ m_scriptPath(),
+ m_addon(),
+ m_lastModified(),
+ m_requestData(),
+ m_responseData(),
+ m_responseRanges(),
+ m_redirectUrl()
+{ }
+
+CHTTPPythonHandler::CHTTPPythonHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request),
+ m_scriptPath(),
+ m_addon(),
+ m_lastModified(),
+ m_requestData(),
+ m_responseData(),
+ m_responseRanges(),
+ m_redirectUrl()
+{
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+
+ // get the real path of the script and check if it actually exists
+ m_response.status = CHTTPWebinterfaceHandler::ResolveUrl(m_request.pathUrl, m_scriptPath, m_addon);
+ // only allow requests to a non-static webinterface addon
+ if (m_addon == NULL || m_addon->Type() != ADDON::AddonType::WEB_INTERFACE ||
+ std::dynamic_pointer_cast<ADDON::CWebinterface>(m_addon)->GetType() ==
+ ADDON::WebinterfaceTypeStatic)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ return;
+ }
+
+ std::shared_ptr<ADDON::CWebinterface> webinterface = std::dynamic_pointer_cast<ADDON::CWebinterface>(m_addon);
+
+ // forward every request to the default entry point
+ m_scriptPath = webinterface->LibPath();
+
+ // we need to map any requests to a specific WSGI webinterface to the root path
+ std::string baseLocation = webinterface->GetBaseLocation();
+ if (!URIUtils::PathHasParent(m_request.pathUrl, baseLocation))
+ {
+ m_response.type = HTTPRedirect;
+ m_response.status = MHD_HTTP_MOVED_PERMANENTLY;
+ m_redirectUrl = baseLocation + m_request.pathUrl;
+ }
+
+ // no need to try to read the last modified date from a non-existing file
+ if (m_response.status != MHD_HTTP_OK)
+ return;
+
+ // determine the last modified date
+ const CURL pathToUrl(m_scriptPath);
+ struct __stat64 statBuffer;
+ if (XFILE::CFile::Stat(pathToUrl, &statBuffer) != 0)
+ return;
+
+ struct tm* time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer.st_mtime);
+#endif
+ if (time == NULL)
+ return;
+
+ m_lastModified = *time;
+}
+
+bool CHTTPPythonHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ ADDON::AddonPtr addon;
+ std::string path;
+ // try to resolve the addon as any python script must be part of a webinterface
+ if (!CHTTPWebinterfaceHandler::ResolveAddon(request.pathUrl, addon, path) || addon == NULL ||
+ addon->Type() != ADDON::AddonType::WEB_INTERFACE)
+ return false;
+
+ // static webinterfaces aren't allowed to run python scripts
+ ADDON::CWebinterface* webinterface = static_cast<ADDON::CWebinterface*>(addon.get());
+ if (webinterface->GetType() != ADDON::WebinterfaceTypeWsgi)
+ return false;
+
+ return true;
+}
+
+MHD_RESULT CHTTPPythonHandler::HandleRequest()
+{
+ if (m_response.type == HTTPError || m_response.type == HTTPRedirect)
+ return MHD_YES;
+
+ std::vector<std::string> args;
+ args.push_back(m_scriptPath);
+
+ try
+ {
+ HTTPPythonRequest* pythonRequest = new HTTPPythonRequest();
+ pythonRequest->connection = m_request.connection;
+ pythonRequest->file = URIUtils::GetFileName(m_request.pathUrl);
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, pythonRequest->getValues);
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_HEADER_KIND, pythonRequest->headerValues);
+ pythonRequest->method = m_request.method;
+ pythonRequest->postValues = m_postFields;
+ pythonRequest->requestContent = m_requestData;
+ pythonRequest->responseType = HTTPNone;
+ pythonRequest->responseLength = 0;
+ pythonRequest->responseStatus = MHD_HTTP_OK;
+ pythonRequest->url = m_request.pathUrlFull;
+ pythonRequest->path = m_request.pathUrl;
+ pythonRequest->version = m_request.version;
+ pythonRequest->requestTime = CDateTime::GetCurrentDateTime();
+ pythonRequest->lastModifiedTime = m_lastModified;
+
+ std::string hostname;
+ uint16_t port;
+ if (GetHostnameAndPort(hostname, port))
+ {
+ pythonRequest->hostname = hostname;
+ pythonRequest->port = port;
+ }
+
+ CHTTPPythonInvoker* pythonInvoker =
+ new CHTTPPythonWsgiInvoker(&CServiceBroker::GetXBPython(), pythonRequest);
+ LanguageInvokerPtr languageInvokerPtr(pythonInvoker);
+ int result = CScriptInvocationManager::GetInstance().ExecuteSync(m_scriptPath, languageInvokerPtr, m_addon, args, 30000, false);
+
+ // check if the script couldn't be started
+ if (result < 0)
+ {
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.type = HTTPError;
+
+ return MHD_YES;
+ }
+ // check if the script exited with an error
+ if (result > 0)
+ {
+ // check if the script didn't finish in time
+ if (result == ETIMEDOUT)
+ m_response.status = MHD_HTTP_REQUEST_TIMEOUT;
+ else
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.type = HTTPError;
+
+ return MHD_YES;
+ }
+
+ HTTPPythonRequest* pythonFinalizedRequest = pythonInvoker->GetRequest();
+ if (pythonFinalizedRequest == NULL)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return MHD_YES;
+ }
+
+ m_response.type = pythonFinalizedRequest->responseType;
+ m_response.status = pythonFinalizedRequest->responseStatus;
+ if (m_response.status < MHD_HTTP_BAD_REQUEST)
+ {
+ if (m_response.type == HTTPNone)
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.headers = pythonFinalizedRequest->responseHeaders;
+
+ if (pythonFinalizedRequest->lastModifiedTime.IsValid())
+ m_lastModified = pythonFinalizedRequest->lastModifiedTime;
+ }
+ else
+ {
+ if (m_response.type == HTTPNone)
+ m_response.type = HTTPError;
+ m_response.headers = pythonFinalizedRequest->responseHeadersError;
+ }
+
+ m_responseData = pythonFinalizedRequest->responseData;
+ if (pythonFinalizedRequest->responseLength > 0 && pythonFinalizedRequest->responseLength <= m_responseData.size())
+ m_response.totalLength = pythonFinalizedRequest->responseLength;
+ else
+ m_response.totalLength = m_responseData.size();
+
+ CHttpResponseRange responseRange(m_responseData.c_str(), m_responseData.size());
+ m_responseRanges.push_back(responseRange);
+
+ if (!pythonFinalizedRequest->responseContentType.empty())
+ m_response.contentType = pythonFinalizedRequest->responseContentType;
+ }
+ catch (...)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ return MHD_YES;
+}
+
+bool CHTTPPythonHandler::GetLastModifiedDate(CDateTime &lastModified) const
+{
+ if (!m_lastModified.IsValid())
+ return false;
+
+ lastModified = m_lastModified;
+ return true;
+}
+
+bool CHTTPPythonHandler::appendPostData(const char *data, size_t size)
+{
+ if (m_requestData.size() + size > MAX_STRING_POST_SIZE)
+ {
+ CServiceBroker::GetLogging()
+ .GetLogger("CHTTPPythonHandler")
+ ->error("Stopped uploading post since it exceeded size limitations ({})",
+ MAX_STRING_POST_SIZE);
+ return false;
+ }
+
+ m_requestData.append(data, size);
+
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPPythonHandler.h b/xbmc/network/httprequesthandler/HTTPPythonHandler.h
new file mode 100644
index 0000000..166430e
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPPythonHandler.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "addons/IAddon.h"
+#include "addons/Webinterface.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+class CHTTPPythonHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPPythonHandler();
+ ~CHTTPPythonHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPPythonHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+ bool CanHandleRanges() const override { return false; }
+ bool CanBeCached() const override { return false; }
+ bool GetLastModifiedDate(CDateTime &lastModified) const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ HttpResponseRanges GetResponseData() const override { return m_responseRanges; }
+
+ std::string GetRedirectUrl() const override { return m_redirectUrl; }
+
+ int GetPriority() const override { return 3; }
+
+protected:
+ explicit CHTTPPythonHandler(const HTTPRequest &request);
+
+ bool appendPostData(const char *data, size_t size) override;
+
+private:
+ std::string m_scriptPath;
+ ADDON::AddonPtr m_addon;
+ CDateTime m_lastModified;
+
+ std::string m_requestData;
+ std::string m_responseData;
+ HttpResponseRanges m_responseRanges;
+
+ std::string m_redirectUrl;
+};
diff --git a/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp
new file mode 100644
index 0000000..240449a
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPRequestHandlerUtils.h"
+
+#include "utils/StringUtils.h"
+
+#include <map>
+
+std::string HTTPRequestHandlerUtils::GetRequestHeaderValue(struct MHD_Connection *connection, enum MHD_ValueKind kind, const std::string &key)
+{
+ if (connection == nullptr)
+ return "";
+
+ const char* value = MHD_lookup_connection_value(connection, kind, key.c_str());
+ if (value == nullptr)
+ return "";
+
+ if (StringUtils::EqualsNoCase(key, MHD_HTTP_HEADER_CONTENT_TYPE))
+ {
+ // Work around a bug in firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=416178)
+ // by cutting of anything that follows a ";" in a "Content-Type" header field
+ std::string strValue(value);
+ size_t pos = strValue.find(';');
+ if (pos != std::string::npos)
+ strValue = strValue.substr(0, pos);
+
+ return strValue;
+ }
+
+ return value;
+}
+
+int HTTPRequestHandlerUtils::GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::map<std::string, std::string> &headerValues)
+{
+ if (connection == nullptr)
+ return -1;
+
+ return MHD_get_connection_values(connection, kind, FillArgumentMap, &headerValues);
+}
+
+int HTTPRequestHandlerUtils::GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::multimap<std::string, std::string> &headerValues)
+{
+ if (connection == nullptr)
+ return -1;
+
+ return MHD_get_connection_values(connection, kind, FillArgumentMultiMap, &headerValues);
+}
+
+bool HTTPRequestHandlerUtils::GetRequestedRanges(struct MHD_Connection *connection, uint64_t totalLength, CHttpRanges &ranges)
+{
+ ranges.Clear();
+
+ if (connection == nullptr)
+ return false;
+
+ return ranges.Parse(GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE), totalLength);
+}
+
+MHD_RESULT HTTPRequestHandlerUtils::FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value)
+{
+ if (cls == nullptr || key == nullptr)
+ return MHD_NO;
+
+ std::map<std::string, std::string> *arguments = reinterpret_cast<std::map<std::string, std::string>*>(cls);
+ arguments->insert(std::make_pair(key, value != nullptr ? value : ""));
+
+ return MHD_YES;
+}
+
+MHD_RESULT HTTPRequestHandlerUtils::FillArgumentMultiMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value)
+{
+ if (cls == nullptr || key == nullptr)
+ return MHD_NO;
+
+ std::multimap<std::string, std::string> *arguments = reinterpret_cast<std::multimap<std::string, std::string>*>(cls);
+ arguments->insert(std::make_pair(key, value != nullptr ? value : ""));
+
+ return MHD_YES;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h
new file mode 100644
index 0000000..d02b5c1
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <stdint.h>
+#include <string>
+
+class HTTPRequestHandlerUtils
+{
+public:
+ static std::string GetRequestHeaderValue(struct MHD_Connection *connection, enum MHD_ValueKind kind, const std::string &key);
+ static int GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::map<std::string, std::string> &headerValues);
+ static int GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::multimap<std::string, std::string> &headerValues);
+
+ static bool GetRequestedRanges(struct MHD_Connection *connection, uint64_t totalLength, CHttpRanges &ranges);
+
+private:
+ HTTPRequestHandlerUtils() = delete;
+
+ static MHD_RESULT FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value);
+ static MHD_RESULT FillArgumentMultiMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value);
+};
diff --git a/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp b/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp
new file mode 100644
index 0000000..ef2f7a6
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011-2020 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPVfsHandler.h"
+
+#include "MediaSource.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "media/MediaLockState.h"
+#include "settings/MediaSourceSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+
+CHTTPVfsHandler::CHTTPVfsHandler(const HTTPRequest &request)
+ : CHTTPFileHandler(request)
+{
+ std::string file;
+ int responseStatus = MHD_HTTP_BAD_REQUEST;
+
+ if (m_request.pathUrl.size() > 5)
+ {
+ file = m_request.pathUrl.substr(5);
+
+ if (CFileUtils::Exists(file))
+ {
+ bool accessible = false;
+ if (file.substr(0, 8) == "image://")
+ accessible = true;
+ else
+ {
+ std::string sourceTypes[] = { "video", "music", "pictures" };
+ unsigned int size = sizeof(sourceTypes) / sizeof(std::string);
+
+ std::string realPath = URIUtils::GetRealPath(file);
+ // for rar:// and zip:// paths we need to extract the path to the archive instead of using the VFS path
+ while (URIUtils::IsInArchive(realPath))
+ realPath = CURL(realPath).GetHostName();
+
+ // Check manually configured sources
+ VECSOURCES *sources = NULL;
+ for (unsigned int index = 0; index < size && !accessible; index++)
+ {
+ sources = CMediaSourceSettings::GetInstance().GetSources(sourceTypes[index]);
+ if (sources == NULL)
+ continue;
+
+ for (const auto& source : *sources)
+ {
+ if (accessible)
+ break;
+
+ // don't allow access to locked / disabled sharing sources
+ if (source.m_iHasLock == LOCK_STATE_LOCKED || !source.m_allowSharing)
+ continue;
+
+ for (const auto& path : source.vecPaths)
+ {
+ std::string realSourcePath = URIUtils::GetRealPath(path);
+ if (URIUtils::PathHasParent(realPath, realSourcePath, true))
+ {
+ accessible = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // Check auto-mounted sources
+ if (!accessible)
+ {
+ bool isSource;
+ VECSOURCES removableSources;
+ CServiceBroker::GetMediaManager().GetRemovableDrives(removableSources);
+ int sourceIndex = CUtil::GetMatchingSource(realPath, removableSources, isSource);
+ if (sourceIndex >= 0 && sourceIndex < static_cast<int>(removableSources.size()) &&
+ removableSources.at(sourceIndex).m_iHasLock != LOCK_STATE_LOCKED &&
+ removableSources.at(sourceIndex).m_allowSharing)
+ accessible = true;
+ }
+ }
+
+ if (accessible)
+ responseStatus = MHD_HTTP_OK;
+ // the file exists but not in one of the defined sources so we deny access to it
+ else
+ responseStatus = MHD_HTTP_UNAUTHORIZED;
+ }
+ else
+ responseStatus = MHD_HTTP_NOT_FOUND;
+ }
+
+ // set the file and the HTTP response status
+ SetFile(file, responseStatus);
+}
+
+bool CHTTPVfsHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return request.pathUrl.find("/vfs") == 0;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPVfsHandler.h b/xbmc/network/httprequesthandler/HTTPVfsHandler.h
new file mode 100644
index 0000000..af66bad
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPVfsHandler.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/httprequesthandler/HTTPFileHandler.h"
+
+#include <string>
+
+class CHTTPVfsHandler : public CHTTPFileHandler
+{
+public:
+ CHTTPVfsHandler() = default;
+ ~CHTTPVfsHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPVfsHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ int GetPriority() const override { return 5; }
+
+protected:
+ explicit CHTTPVfsHandler(const HTTPRequest &request);
+};
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp
new file mode 100644
index 0000000..fa47144
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPWebinterfaceAddonsHandler.h"
+
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "network/WebServer.h"
+
+#define ADDON_HEADER "<html><head><title>Add-on List</title></head><body>\n<h1>Available web interfaces:</h1>\n<ul>\n"
+
+bool CHTTPWebinterfaceAddonsHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return (request.pathUrl.compare("/addons") == 0 || request.pathUrl.compare("/addons/") == 0);
+}
+
+MHD_RESULT CHTTPWebinterfaceAddonsHandler::HandleRequest()
+{
+ m_responseData = ADDON_HEADER;
+ ADDON::VECADDONS addons;
+ if (!CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::WEB_INTERFACE) ||
+ addons.empty())
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ return MHD_YES;
+ }
+
+ for (const auto& addon : addons)
+ m_responseData += "<li><a href=/addons/" + addon->ID() + "/>" + addon->Name() + "</a></li>\n";
+
+ m_responseData += "</ul>\n</body></html>";
+
+ m_responseRange.SetData(m_responseData.c_str(), m_responseData.size());
+
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.status = MHD_HTTP_OK;
+ m_response.contentType = "text/html";
+ m_response.totalLength = m_responseData.size();
+
+ return MHD_YES;
+}
+
+HttpResponseRanges CHTTPWebinterfaceAddonsHandler::GetResponseData() const
+{
+ HttpResponseRanges ranges;
+ ranges.push_back(m_responseRange);
+
+ return ranges;
+}
+
+
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h
new file mode 100644
index 0000000..20a44ec
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <string>
+
+class CHTTPWebinterfaceAddonsHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPWebinterfaceAddonsHandler() = default;
+ ~CHTTPWebinterfaceAddonsHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPWebinterfaceAddonsHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ HttpResponseRanges GetResponseData() const override;
+
+ int GetPriority() const override { return 4; }
+
+protected:
+ explicit CHTTPWebinterfaceAddonsHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request)
+ { }
+
+private:
+ std::string m_responseData;
+ CHttpResponseRange m_responseRange;
+};
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp
new file mode 100644
index 0000000..fe6e760
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPWebinterfaceHandler.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/Directory.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#define WEBSERVER_DIRECTORY_SEPARATOR "/"
+
+CHTTPWebinterfaceHandler::CHTTPWebinterfaceHandler(const HTTPRequest &request)
+ : CHTTPFileHandler(request)
+{
+ // resolve the URL into a file path and a HTTP response status
+ std::string file;
+ int responseStatus = ResolveUrl(request.pathUrl, file);
+
+ // set the file and the HTTP response status
+ SetFile(file, responseStatus);
+}
+
+bool CHTTPWebinterfaceHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return true;
+}
+
+int CHTTPWebinterfaceHandler::ResolveUrl(const std::string &url, std::string &path)
+{
+ ADDON::AddonPtr dummyAddon;
+ return ResolveUrl(url, path, dummyAddon);
+}
+
+int CHTTPWebinterfaceHandler::ResolveUrl(const std::string &url, std::string &path, ADDON::AddonPtr &addon)
+{
+ // determine the addon and addon's path
+ if (!ResolveAddon(url, addon, path))
+ return MHD_HTTP_NOT_FOUND;
+
+ if (XFILE::CDirectory::Exists(path))
+ {
+ if (URIUtils::GetFileName(path).empty())
+ {
+ // determine the actual file path using the default entry point
+ if (addon != NULL && addon->Type() == ADDON::AddonType::WEB_INTERFACE)
+ path = std::dynamic_pointer_cast<ADDON::CWebinterface>(addon)->GetEntryPoint(path);
+ }
+ else
+ {
+ URIUtils::AddSlashAtEnd(path);
+ return MHD_HTTP_FOUND;
+ }
+ }
+
+ if (!CFileUtils::CheckFileAccessAllowed(path))
+ return MHD_HTTP_NOT_FOUND;
+
+ if (!CFileUtils::Exists(path))
+ return MHD_HTTP_NOT_FOUND;
+
+ return MHD_HTTP_OK;
+}
+
+bool CHTTPWebinterfaceHandler::ResolveAddon(const std::string &url, ADDON::AddonPtr &addon)
+{
+ std::string addonPath;
+ return ResolveAddon(url, addon, addonPath);
+}
+
+bool CHTTPWebinterfaceHandler::ResolveAddon(const std::string &url, ADDON::AddonPtr &addon, std::string &addonPath)
+{
+ std::string path = url;
+
+ // check if the URL references a specific addon
+ if (url.find("/addons/") == 0 && url.size() > 8)
+ {
+ std::vector<std::string> components;
+ StringUtils::Tokenize(path, components, WEBSERVER_DIRECTORY_SEPARATOR);
+ if (components.size() <= 1)
+ return false;
+
+ if (!CServiceBroker::GetAddonMgr().GetAddon(components.at(1), addon,
+ ADDON::OnlyEnabled::CHOICE_YES) ||
+ addon == NULL)
+ return false;
+
+ addonPath = addon->Path();
+ if (addon->Type() !=
+ ADDON::AddonType::WEB_INTERFACE) // No need to append /htdocs for web interfaces
+ addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/");
+
+ // remove /addons/<addon-id> from the path
+ components.erase(components.begin(), components.begin() + 2);
+
+ // determine the path within the addon
+ path = StringUtils::Join(components, WEBSERVER_DIRECTORY_SEPARATOR);
+ }
+ else if (!ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::WEB_INTERFACE,
+ addon) ||
+ addon == NULL)
+ return false;
+
+ // get the path of the addon
+ addonPath = addon->Path();
+
+ // add /htdocs/ to the addon's path if it's not a webinterface
+ if (addon->Type() != ADDON::AddonType::WEB_INTERFACE)
+ addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/");
+
+ // append the path within the addon to the path of the addon
+ addonPath = URIUtils::AddFileToFolder(addonPath, path);
+
+ // ensure that we don't have a directory traversal hack here
+ // by checking if the resolved absolute path is inside the addon path
+ std::string realPath = URIUtils::GetRealPath(addonPath);
+ std::string realAddonPath = URIUtils::GetRealPath(addon->Path());
+ if (!URIUtils::PathHasParent(realPath, realAddonPath, true))
+ return false;
+
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h
new file mode 100644
index 0000000..5618c75
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/IAddon.h"
+#include "network/httprequesthandler/HTTPFileHandler.h"
+
+#include <string>
+
+class CHTTPWebinterfaceHandler : public CHTTPFileHandler
+{
+public:
+ CHTTPWebinterfaceHandler() = default;
+ ~CHTTPWebinterfaceHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPWebinterfaceHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ static int ResolveUrl(const std::string &url, std::string &path);
+ static int ResolveUrl(const std::string &url, std::string &path, ADDON::AddonPtr &addon);
+ static bool ResolveAddon(const std::string &url, ADDON::AddonPtr &addon);
+ static bool ResolveAddon(const std::string &url, ADDON::AddonPtr &addon, std::string &addonPath);
+
+protected:
+ explicit CHTTPWebinterfaceHandler(const HTTPRequest &request);
+};
diff --git a/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp b/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp
new file mode 100644
index 0000000..3a06604
--- /dev/null
+++ b/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "IHTTPRequestHandler.h"
+
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "utils/StringUtils.h"
+
+#include <limits>
+#include <utility>
+
+static const std::string HTTPMethodHead = "HEAD";
+static const std::string HTTPMethodGet = "GET";
+static const std::string HTTPMethodPost = "POST";
+
+HTTPMethod GetHTTPMethod(const char *method)
+{
+ if (HTTPMethodGet.compare(method) == 0)
+ return GET;
+ if (HTTPMethodPost.compare(method) == 0)
+ return POST;
+ if (HTTPMethodHead.compare(method) == 0)
+ return HEAD;
+
+ return UNKNOWN;
+}
+
+std::string GetHTTPMethod(HTTPMethod method)
+{
+ switch (method)
+ {
+ case HEAD:
+ return HTTPMethodHead;
+
+ case GET:
+ return HTTPMethodGet;
+
+ case POST:
+ return HTTPMethodPost;
+
+ case UNKNOWN:
+ break;
+ }
+
+ return "";
+}
+
+IHTTPRequestHandler::IHTTPRequestHandler()
+ : m_request(),
+ m_response(),
+ m_postFields()
+{ }
+
+IHTTPRequestHandler::IHTTPRequestHandler(const HTTPRequest &request)
+ : m_request(request),
+ m_response(),
+ m_postFields()
+{
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.totalLength = 0;
+}
+
+bool IHTTPRequestHandler::HasResponseHeader(const std::string &field) const
+{
+ if (field.empty())
+ return false;
+
+ return m_response.headers.find(field) != m_response.headers.end();
+}
+
+bool IHTTPRequestHandler::AddResponseHeader(const std::string &field, const std::string &value, bool allowMultiple /* = false */)
+{
+ if (field.empty() || value.empty())
+ return false;
+
+ if (!allowMultiple && HasResponseHeader(field))
+ return false;
+
+ m_response.headers.insert(std::make_pair(field, value));
+ return true;
+}
+
+void IHTTPRequestHandler::AddPostField(const std::string &key, const std::string &value)
+{
+ if (key.empty())
+ return;
+
+ std::map<std::string, std::string>::iterator field = m_postFields.find(key);
+ if (field == m_postFields.end())
+ m_postFields[key] = value;
+ else
+ m_postFields[key].append(value);
+}
+
+bool IHTTPRequestHandler::AddPostData(const char *data, size_t size)
+{
+ if (size > 0)
+ return appendPostData(data, size);
+
+ return true;
+}
+
+bool IHTTPRequestHandler::GetRequestedRanges(uint64_t totalLength)
+{
+ if (!m_ranged || m_request.webserver == NULL || m_request.connection == NULL)
+ return false;
+
+ m_request.ranges.Clear();
+ if (totalLength == 0)
+ return true;
+
+ return HTTPRequestHandlerUtils::GetRequestedRanges(m_request.connection, totalLength, m_request.ranges);
+}
+
+bool IHTTPRequestHandler::GetHostnameAndPort(std::string& hostname, uint16_t &port)
+{
+ if (m_request.webserver == NULL || m_request.connection == NULL)
+ return false;
+
+ std::string hostnameAndPort = HTTPRequestHandlerUtils::GetRequestHeaderValue(m_request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_HOST);
+ if (hostnameAndPort.empty())
+ return false;
+
+ size_t pos = hostnameAndPort.find(':');
+ hostname = hostnameAndPort.substr(0, pos);
+ if (hostname.empty())
+ return false;
+
+ if (pos != std::string::npos)
+ {
+ std::string strPort = hostnameAndPort.substr(pos + 1);
+ if (!StringUtils::IsNaturalNumber(strPort))
+ return false;
+
+ unsigned long portL = strtoul(strPort.c_str(), NULL, 0);
+ if (portL > std::numeric_limits<uint16_t>::max())
+ return false;
+
+ port = static_cast<uint16_t>(portL);
+ }
+ else
+ port = 80;
+
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/IHTTPRequestHandler.h b/xbmc/network/httprequesthandler/IHTTPRequestHandler.h
new file mode 100644
index 0000000..13c170f
--- /dev/null
+++ b/xbmc/network/httprequesthandler/IHTTPRequestHandler.h
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "utils/HttpRangeUtils.h"
+
+#include <map>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+
+#include <microhttpd.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#if MHD_VERSION >= 0x00097002
+using MHD_RESULT = MHD_Result;
+#else
+using MHD_RESULT = int;
+#endif
+
+class CDateTime;
+class CWebServer;
+
+enum HTTPMethod
+{
+ UNKNOWN,
+ POST,
+ GET,
+ HEAD
+};
+
+HTTPMethod GetHTTPMethod(const char *method);
+std::string GetHTTPMethod(HTTPMethod method);
+
+typedef enum HTTPResponseType
+{
+ HTTPNone,
+ // creates and returns a HTTP error
+ HTTPError,
+ // creates and returns a HTTP redirect response
+ HTTPRedirect,
+ // creates a HTTP response with the content from a file
+ HTTPFileDownload,
+ // creates a HTTP response from a buffer without copying or freeing the buffer
+ HTTPMemoryDownloadNoFreeNoCopy,
+ // creates a HTTP response from a buffer by copying but not freeing the buffer
+ HTTPMemoryDownloadNoFreeCopy,
+ // creates a HTTP response from a buffer without copying followed by freeing the buffer
+ // the buffer must have been malloc'ed and not new'ed
+ HTTPMemoryDownloadFreeNoCopy,
+ // creates a HTTP response from a buffer by copying followed by freeing the buffer
+ // the buffer must have been malloc'ed and not new'ed
+ HTTPMemoryDownloadFreeCopy
+} HTTPResponseType;
+
+typedef struct HTTPRequest
+{
+ CWebServer *webserver;
+ struct MHD_Connection *connection;
+ std::string pathUrlFull;
+ std::string pathUrl;
+ HTTPMethod method;
+ std::string version;
+ CHttpRanges ranges;
+} HTTPRequest;
+
+typedef struct HTTPResponseDetails {
+ HTTPResponseType type;
+ int status;
+ std::multimap<std::string, std::string> headers;
+ std::string contentType;
+ uint64_t totalLength;
+} HTTPResponseDetails;
+
+class IHTTPRequestHandler
+{
+public:
+ virtual ~IHTTPRequestHandler() = default;
+
+ /*!
+ * \brief Creates a new HTTP request handler for the given request.
+ *
+ * \details This call is responsible for doing some preparation work like -
+ * depending on the supported features - determining whether the requested
+ * entity supports ranges, whether it can be cached and what it's last
+ * modified date is.
+ *
+ * \param request HTTP request to be handled
+ */
+ virtual IHTTPRequestHandler* Create(const HTTPRequest &request) const = 0;
+
+ /*!
+ * \brief Returns the priority of the HTTP request handler.
+ *
+ * \details The higher the priority the more important is the HTTP request
+ * handler.
+ */
+ virtual int GetPriority() const { return 0; }
+
+ /*!
+ * \brief Checks if the HTTP request handler can handle the given request.
+ *
+ * \param request HTTP request to be handled
+ * \return True if the given HTTP request can be handled otherwise false.
+ */
+ virtual bool CanHandleRequest(const HTTPRequest &request) const = 0;
+
+ /*!
+ * \brief Handles the HTTP request.
+ *
+ * \return MHD_NO if a severe error has occurred otherwise MHD_YES.
+ */
+ virtual MHD_RESULT HandleRequest() = 0;
+
+ /*!
+ * \brief Whether the HTTP response could also be provided in ranges.
+ */
+ virtual bool CanHandleRanges() const { return false; }
+
+ /*!
+ * \brief Whether the HTTP response can be cached.
+ */
+ virtual bool CanBeCached() const { return false; }
+
+ /*!
+ * \brief Returns the maximum age (in seconds) for which the response can be cached.
+ *
+ * \details This is only used if the response can be cached.
+ */
+ virtual int GetMaximumAgeForCaching() const { return 0; }
+
+ /*!
+ * \brief Returns the last modification date of the response data.
+ *
+ * \details This is only used if the response can be cached.
+ */
+ virtual bool GetLastModifiedDate(CDateTime &lastModified) const { return false; }
+
+ /*!
+ * \brief Returns the ranges with raw data belonging to the response.
+ *
+ * \details This is only used if the response type is one of the HTTPMemoryDownload types.
+ */
+ virtual HttpResponseRanges GetResponseData() const { return HttpResponseRanges(); }
+
+ /*!
+ * \brief Returns the URL to which the request should be redirected.
+ *
+ * \details This is only used if the response type is HTTPRedirect.
+ */
+ virtual std::string GetRedirectUrl() const { return ""; }
+
+ /*!
+ * \brief Returns the path to the file that should be returned as the response.
+ *
+ * \details This is only used if the response type is HTTPFileDownload.
+ */
+ virtual std::string GetResponseFile() const { return ""; }
+
+ /*!
+ * \brief Returns the HTTP request handled by the HTTP request handler.
+ */
+ const HTTPRequest& GetRequest() const { return m_request; }
+
+ /*!
+ * \brief Returns true if the HTTP request is ranged, otherwise false.
+ */
+ bool IsRequestRanged() const { return m_ranged; }
+
+ /*!
+ * \brief Sets whether the HTTP request contains ranges or not
+ */
+ void SetRequestRanged(bool ranged) { m_ranged = ranged; }
+
+ /*!
+ * \brief Sets the response status of the HTTP response.
+ *
+ * \param status HTTP status of the response
+ */
+ void SetResponseStatus(int status) { m_response.status = status; }
+
+ /*!
+ * \brief Checks if the given HTTP header field is part of the response details.
+ *
+ * \param field HTTP header field name
+ * \return True if the header field is set, otherwise false.
+ */
+ bool HasResponseHeader(const std::string &field) const;
+
+ /*!
+ * \brief Adds the given HTTP header field and value to the response details.
+ *
+ * \param field HTTP header field name
+ * \param value HTTP header field value
+ * \param allowMultiple Whether the same header is allowed multiple times
+ * \return True if the header field was added, otherwise false.
+ */
+ bool AddResponseHeader(const std::string &field, const std::string &value, bool allowMultiple = false);
+
+ /*!
+ * \brief Returns the HTTP response header details.
+ */
+ const HTTPResponseDetails& GetResponseDetails() const { return m_response; }
+
+ /*!
+ * \brief Adds the given key-value pair extracted from the HTTP POST data.
+ *
+ * \param key Key of the HTTP POST field
+ * \param value Value of the HTTP POST field
+ */
+ void AddPostField(const std::string &key, const std::string &value);
+ /*!
+ * \brief Adds the given raw HTTP POST data.
+ *
+ * \param data Raw HTTP POST data
+ * \param size Size of the raw HTTP POST data
+ */
+ bool AddPostData(const char *data, size_t size);
+
+protected:
+ IHTTPRequestHandler();
+ explicit IHTTPRequestHandler(const HTTPRequest &request);
+
+ virtual bool appendPostData(const char *data, size_t size)
+ { return true; }
+
+ bool GetRequestedRanges(uint64_t totalLength);
+ bool GetHostnameAndPort(std::string& hostname, uint16_t &port);
+
+ HTTPRequest m_request;
+ HTTPResponseDetails m_response;
+
+ std::map<std::string, std::string> m_postFields;
+
+private:
+ bool m_ranged = false;
+};
diff --git a/xbmc/network/httprequesthandler/python/CMakeLists.txt b/xbmc/network/httprequesthandler/python/CMakeLists.txt
new file mode 100644
index 0000000..7bbbad9
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/CMakeLists.txt
@@ -0,0 +1,10 @@
+if(MICROHTTPD_FOUND AND PYTHON_FOUND)
+ set(SOURCES HTTPPythonInvoker.cpp
+ HTTPPythonWsgiInvoker.cpp)
+
+ set(HEADERS HTTPPythonInvoker.h
+ HTTPPythonRequest.h
+ HTTPPythonWsgiInvoker.h)
+
+ core_add_library(network_httprequesthandlers_python)
+endif()
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp
new file mode 100644
index 0000000..696c039
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPPythonInvoker.h"
+
+#include "CompileInfo.h"
+#include "utils/StringUtils.h"
+
+CHTTPPythonInvoker::CHTTPPythonInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request)
+ : CPythonInvoker(invocationHandler),
+ m_request(request),
+ m_internalError(false)
+{ }
+
+CHTTPPythonInvoker::~CHTTPPythonInvoker()
+{
+ delete m_request;
+ m_request = NULL;
+}
+
+void CHTTPPythonInvoker::onAbort()
+{
+ if (m_request == NULL)
+ return;
+
+ m_internalError = true;
+ m_request->responseType = HTTPError;
+ m_request->responseStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+}
+
+void CHTTPPythonInvoker::onError(const std::string& exceptionType /* = "" */, const std::string& exceptionValue /* = "" */, const std::string& exceptionTraceback /* = "" */)
+{
+ if (m_request == NULL)
+ return;
+
+ m_internalError = true;
+ m_request->responseType = HTTPMemoryDownloadNoFreeCopy;
+ m_request->responseStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ std::string output;
+ if (!exceptionType.empty())
+ {
+ output += exceptionType;
+
+ if (!exceptionValue.empty())
+ output += ": " + exceptionValue;
+ output += "\n";
+ }
+
+ if (!exceptionTraceback .empty())
+ output += exceptionTraceback;
+
+ // replace all special characters
+
+ StringUtils::Replace(output, "<", "&lt;");
+ StringUtils::Replace(output, ">", "&gt;");
+ StringUtils::Replace(output, " ", "&nbsp;");
+ StringUtils::Replace(output, "\n", "\n<br />");
+
+ if (!exceptionType.empty())
+ {
+ // now make the type and value bold (needs to be done here because otherwise the < and > would have been replaced
+ output = "<b>" + output;
+ output.insert(output.find('\n'), "</b>");
+ }
+
+ m_request->responseData = "<html><head><title>" + std::string(CCompileInfo::GetAppName()) + ": python error</title></head><body>" + output + "</body></html>";
+}
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h
new file mode 100644
index 0000000..83f1f34
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/python/PythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+
+#include <string>
+
+class CHTTPPythonInvoker : public CPythonInvoker
+{
+public:
+ ~CHTTPPythonInvoker() override;
+
+ virtual HTTPPythonRequest* GetRequest() = 0;
+
+protected:
+ CHTTPPythonInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request);
+
+ // overrides of CPythonInvoker
+ void onAbort() override;
+ void onError(const std::string& exceptionType = "", const std::string& exceptionValue = "", const std::string& exceptionTraceback = "") override;
+
+ HTTPPythonRequest* m_request;
+ bool m_internalError;
+};
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h b/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h
new file mode 100644
index 0000000..7874203
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "XBDateTime.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <map>
+#include <stdint.h>
+#include <string>
+
+typedef struct HTTPPythonRequest
+{
+ struct MHD_Connection *connection;
+ std::string hostname;
+ uint16_t port;
+ std::string url;
+ std::string path;
+ std::string file;
+ HTTPMethod method;
+ std::string version;
+ std::multimap<std::string, std::string> headerValues;
+ std::map<std::string, std::string> getValues;
+ std::map<std::string, std::string> postValues;
+ std::string requestContent;
+ CDateTime requestTime;
+ CDateTime lastModifiedTime;
+
+ HTTPResponseType responseType;
+ int responseStatus;
+ std::string responseContentType;
+ std::string responseData;
+ size_t responseLength;
+ std::multimap<std::string, std::string> responseHeaders;
+ std::multimap<std::string, std::string> responseHeadersError;
+} HTTPPythonRequest;
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp
new file mode 100644
index 0000000..ac047a7
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "HTTPPythonWsgiInvoker.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonType.h"
+#include "interfaces/legacy/wsgi/WsgiErrorStream.h"
+#include "interfaces/legacy/wsgi/WsgiInputStream.h"
+#include "interfaces/legacy/wsgi/WsgiResponse.h"
+#include "interfaces/python/swig.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <utility>
+
+#include <Python.h>
+
+#define MODULE "xbmc"
+
+#define RUNSCRIPT_PREAMBLE \
+ "" \
+ "import " MODULE "\n" \
+ "class xbmcout:\n" \
+ " def __init__(self, loglevel=" MODULE ".LOGINFO):\n" \
+ " self.ll=loglevel\n" \
+ " def write(self, data):\n" \
+ " " MODULE ".log(data,self.ll)\n" \
+ " def close(self):\n" \
+ " " MODULE ".log('.')\n" \
+ " def flush(self):\n" \
+ " " MODULE ".log('.')\n" \
+ "import sys\n" \
+ "sys.stdout = xbmcout()\n" \
+ "sys.stderr = xbmcout(" MODULE ".LOGERROR)\n" \
+ ""
+
+#define RUNSCRIPT_SETUPTOOLS_HACK \
+ "" \
+ "import types,sys\n" \
+ "pkg_resources_code = \\\n" \
+ "\"\"\"\n" \
+ "def resource_filename(__name__,__path__):\n" \
+ " return __path__\n" \
+ "\"\"\"\n" \
+ "pkg_resources = types.ModuleType('pkg_resources')\n" \
+ "exec(pkg_resources_code, pkg_resources.__dict__)\n" \
+ "sys.modules['pkg_resources'] = pkg_resources\n" \
+ ""
+
+#define RUNSCRIPT_POSTSCRIPT \
+ MODULE ".log('-->HTTP Python WSGI Interpreter Initialized<--', " MODULE ".LOGINFO)\n" \
+ ""
+
+#if defined(TARGET_ANDROID)
+#define RUNSCRIPT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUPTOOLS_HACK RUNSCRIPT_POSTSCRIPT
+#else
+#define RUNSCRIPT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_POSTSCRIPT
+#endif
+
+namespace PythonBindings {
+PyObject* PyInit_Module_xbmc(void);
+PyObject* PyInit_Module_xbmcaddon(void);
+PyObject* PyInit_Module_xbmcwsgi(void);
+}
+
+using namespace PythonBindings;
+
+typedef struct
+{
+ const char *name;
+ CPythonInvoker::PythonModuleInitialization initialization;
+} PythonModule;
+
+static PythonModule PythonModules[] =
+{
+ { "xbmc", PyInit_Module_xbmc },
+ { "xbmcaddon", PyInit_Module_xbmcaddon },
+ { "xbmcwsgi", PyInit_Module_xbmcwsgi }
+};
+
+CHTTPPythonWsgiInvoker::CHTTPPythonWsgiInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request)
+ : CHTTPPythonInvoker(invocationHandler, request),
+ m_wsgiResponse(NULL)
+{
+ PyImport_AppendInittab("xbmc", PyInit_Module_xbmc);
+ PyImport_AppendInittab("xbmcaddon", PyInit_Module_xbmcaddon);
+ PyImport_AppendInittab("xbmcwsgi", PyInit_Module_xbmcwsgi);
+}
+
+CHTTPPythonWsgiInvoker::~CHTTPPythonWsgiInvoker()
+{
+ delete m_wsgiResponse;
+ m_wsgiResponse = NULL;
+}
+
+HTTPPythonRequest* CHTTPPythonWsgiInvoker::GetRequest()
+{
+ if (m_request == NULL || m_wsgiResponse == NULL)
+ return NULL;
+
+ if (m_internalError)
+ return m_request;
+
+ m_wsgiResponse->Finalize(m_request);
+ return m_request;
+}
+
+void CHTTPPythonWsgiInvoker::executeScript(FILE* fp, const std::string& script, PyObject* moduleDict)
+{
+ if (m_request == NULL || m_addon == NULL || m_addon->Type() != ADDON::AddonType::WEB_INTERFACE ||
+ fp == NULL || script.empty() || moduleDict == NULL)
+ return;
+
+ auto logger = CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CHTTPPythonWsgiInvoker[{}]", script));
+
+ ADDON::CWebinterface* webinterface = static_cast<ADDON::CWebinterface*>(m_addon.get());
+ if (webinterface->GetType() != ADDON::WebinterfaceTypeWsgi)
+ {
+ logger->error("trying to execute a non-WSGI script");
+ return;
+ }
+
+ PyObject* pyScript = NULL;
+ PyObject* pyModule = NULL;
+ PyObject* pyEntryPoint = NULL;
+ std::map<std::string, std::string> cgiEnvironment;
+ PyObject* pyEnviron = NULL;
+ PyObject* pyStart_response = NULL;
+ PyObject* pyArgs = NULL;
+ PyObject* pyResult = NULL;
+ PyObject* pyResultIterator = NULL;
+ PyObject* pyIterResult = NULL;
+
+ // get the script
+ std::string scriptName = URIUtils::GetFileName(script);
+ URIUtils::RemoveExtension(scriptName);
+ pyScript = PyUnicode_FromStringAndSize(scriptName.c_str(), scriptName.size());
+ if (pyScript == NULL)
+ {
+ logger->error("failed to convert script to python string");
+ return;
+ }
+
+ // load the script
+ logger->debug("loading script");
+ pyModule = PyImport_Import(pyScript);
+ Py_DECREF(pyScript);
+ if (pyModule == NULL)
+ {
+ logger->error("failed to load WSGI script");
+ return;
+ }
+
+ // get the entry point
+ const std::string& entryPoint = webinterface->EntryPoint();
+ logger->debug(R"(loading entry point "{}")", entryPoint);
+ pyEntryPoint = PyObject_GetAttrString(pyModule, entryPoint.c_str());
+ if (pyEntryPoint == NULL)
+ {
+ logger->error(R"(failed to load entry point "{}")", entryPoint);
+ goto cleanup;
+ }
+
+ // check if the loaded entry point is a callable function
+ if (!PyCallable_Check(pyEntryPoint))
+ {
+ logger->error(R"(defined entry point "{}" is not callable)", entryPoint);
+ goto cleanup;
+ }
+
+ // prepare the WsgiResponse object
+ m_wsgiResponse = new XBMCAddon::xbmcwsgi::WsgiResponse();
+ if (m_wsgiResponse == NULL)
+ {
+ logger->error("failed to create WsgiResponse object");
+ goto cleanup;
+ }
+
+ try
+ {
+ // prepare the start_response callable
+ pyStart_response = PythonBindings::makePythonInstance(m_wsgiResponse, true);
+
+ // create the (CGI) environment dictionary
+ cgiEnvironment = createCgiEnvironment(m_request, m_addon);
+ // and turn it into a python dictionary
+ pyEnviron = PyDict_New();
+ for (const auto& cgiEnv : cgiEnvironment)
+ {
+ PyObject* pyEnvEntry = PyUnicode_FromStringAndSize(cgiEnv.second.c_str(), cgiEnv.second.size());
+ PyDict_SetItemString(pyEnviron, cgiEnv.first.c_str(), pyEnvEntry);
+ Py_DECREF(pyEnvEntry);
+ }
+
+ // add the WSGI-specific environment variables
+ addWsgiEnvironment(m_request, pyEnviron);
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ logger->error("failed to prepare WsgiResponse object with wrong type exception: {}",
+ e.GetExMessage());
+ goto cleanup;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ logger->error("failed to prepare WsgiResponse object with exception: {}", e.GetExMessage());
+ goto cleanup;
+ }
+ catch (...)
+ {
+ logger->error("failed to prepare WsgiResponse object with unknown exception");
+ goto cleanup;
+ }
+
+ // put together the arguments
+ pyArgs = PyTuple_Pack(2, pyEnviron, pyStart_response);
+ Py_DECREF(pyEnviron);
+ Py_DECREF(pyStart_response);
+
+ // call the given handler with the prepared arguments
+ pyResult = PyObject_CallObject(pyEntryPoint, pyArgs);
+ Py_DECREF(pyArgs);
+ if (pyResult == NULL)
+ {
+ logger->error("no result");
+ goto cleanup;
+ }
+
+ // try to get an iterator from the result object
+ pyResultIterator = PyObject_GetIter(pyResult);
+ if (pyResultIterator == NULL || !PyIter_Check(pyResultIterator))
+ {
+ logger->error("result is not iterable");
+ goto cleanup;
+ }
+
+ // go through all the iterables in the result and turn them into strings
+ while ((pyIterResult = PyIter_Next(pyResultIterator)) != NULL)
+ {
+ std::string result;
+ try
+ {
+ PythonBindings::PyXBMCGetUnicodeString(result, pyIterResult, false, "result", "handle_request");
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ logger->error("failed to parse result iterable object with wrong type exception: {}",
+ e.GetExMessage());
+ goto cleanup;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ logger->error("failed to parse result iterable object with exception: {}", e.GetExMessage());
+ goto cleanup;
+ }
+ catch (...)
+ {
+ logger->error("failed to parse result iterable object with unknown exception");
+ goto cleanup;
+ }
+
+ // append the result string to the response
+ m_wsgiResponse->Append(result);
+ }
+
+cleanup:
+ if (pyIterResult != NULL)
+ {
+ Py_DECREF(pyIterResult);
+ }
+ if (pyResultIterator != NULL)
+ {
+ // Call optional close method on iterator
+ if (PyObject_HasAttrString(pyResultIterator, "close") == 1)
+ {
+ if (PyObject_CallMethod(pyResultIterator, "close", NULL) == NULL)
+ logger->error("failed to close iterator object");
+ }
+ Py_DECREF(pyResultIterator);
+ }
+ if (pyResult != NULL)
+ {
+ Py_DECREF(pyResult);
+ }
+ if (pyEntryPoint != NULL)
+ {
+ Py_DECREF(pyEntryPoint);
+ }
+ if (pyModule != NULL)
+ {
+ Py_DECREF(pyModule);
+ }
+}
+
+std::map<std::string, CPythonInvoker::PythonModuleInitialization> CHTTPPythonWsgiInvoker::getModules() const
+{
+ static std::map<std::string, PythonModuleInitialization> modules;
+ if (modules.empty())
+ {
+ for (const PythonModule& pythonModule : PythonModules)
+ modules.insert(std::make_pair(pythonModule.name, pythonModule.initialization));
+ }
+
+ return modules;
+}
+
+const char* CHTTPPythonWsgiInvoker::getInitializationScript() const
+{
+ return RUNSCRIPT;
+}
+
+std::map<std::string, std::string> CHTTPPythonWsgiInvoker::createCgiEnvironment(
+ const HTTPPythonRequest* httpRequest, const ADDON::AddonPtr& addon)
+{
+ std::map<std::string, std::string> environment;
+
+ // REQUEST_METHOD
+ std::string requestMethod;
+ switch (httpRequest->method)
+ {
+ case HEAD:
+ requestMethod = "HEAD";
+ break;
+
+ case POST:
+ requestMethod = "POST";
+ break;
+
+ case GET:
+ default:
+ requestMethod = "GET";
+ break;
+ }
+ environment.insert(std::make_pair("REQUEST_METHOD", requestMethod));
+
+ // SCRIPT_NAME
+ std::string scriptName = std::dynamic_pointer_cast<ADDON::CWebinterface>(addon)->GetBaseLocation();
+ environment.insert(std::make_pair("SCRIPT_NAME", scriptName));
+
+ // PATH_INFO
+ std::string pathInfo = httpRequest->path.substr(scriptName.size());
+ environment.insert(std::make_pair("PATH_INFO", pathInfo));
+
+ // QUERY_STRING
+ size_t iOptions = httpRequest->url.find_first_of('?');
+ if (iOptions != std::string::npos)
+ environment.insert(std::make_pair("QUERY_STRING", httpRequest->url.substr(iOptions+1)));
+ else
+ environment.insert(std::make_pair("QUERY_STRING", ""));
+
+ // CONTENT_TYPE
+ std::string headerValue;
+ std::multimap<std::string, std::string>::const_iterator headerIt = httpRequest->headerValues.find(MHD_HTTP_HEADER_CONTENT_TYPE);
+ if (headerIt != httpRequest->headerValues.end())
+ headerValue = headerIt->second;
+ environment.insert(std::make_pair("CONTENT_TYPE", headerValue));
+
+ // CONTENT_LENGTH
+ headerValue.clear();
+ headerIt = httpRequest->headerValues.find(MHD_HTTP_HEADER_CONTENT_LENGTH);
+ if (headerIt != httpRequest->headerValues.end())
+ headerValue = headerIt->second;
+ environment.insert(std::make_pair("CONTENT_LENGTH", headerValue));
+
+ // SERVER_NAME
+ environment.insert(std::make_pair("SERVER_NAME", httpRequest->hostname));
+
+ // SERVER_PORT
+ environment.insert(std::make_pair("SERVER_PORT", std::to_string(httpRequest->port)));
+
+ // SERVER_PROTOCOL
+ environment.insert(std::make_pair("SERVER_PROTOCOL", httpRequest->version));
+
+ // HTTP_<HEADER_NAME>
+ for (headerIt = httpRequest->headerValues.begin(); headerIt != httpRequest->headerValues.end(); ++headerIt)
+ {
+ std::string headerName = headerIt->first;
+ StringUtils::ToUpper(headerName);
+ environment.insert(std::make_pair("HTTP_" + headerName, headerIt->second));
+ }
+
+ return environment;
+}
+
+void CHTTPPythonWsgiInvoker::addWsgiEnvironment(HTTPPythonRequest* request, void* environment)
+{
+ if (environment == nullptr)
+ return;
+
+ PyObject* pyEnviron = reinterpret_cast<PyObject*>(environment);
+ if (pyEnviron == nullptr)
+ return;
+
+ // WSGI-defined variables
+ {
+ // wsgi.version
+ PyObject* pyValue = Py_BuildValue("(ii)", 1, 0);
+ PyDict_SetItemString(pyEnviron, "wsgi.version", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.url_scheme
+ PyObject* pyValue = PyUnicode_FromStringAndSize("http", 4);
+ PyDict_SetItemString(pyEnviron, "wsgi.url_scheme", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.input
+ XBMCAddon::xbmcwsgi::WsgiInputStream* wsgiInputStream = new XBMCAddon::xbmcwsgi::WsgiInputStream();
+ if (request != NULL)
+ wsgiInputStream->SetRequest(request);
+
+ PythonBindings::prepareForReturn(wsgiInputStream);
+ PyObject* pyWsgiInputStream = PythonBindings::makePythonInstance(wsgiInputStream, false);
+ PyDict_SetItemString(pyEnviron, "wsgi.input", pyWsgiInputStream);
+ Py_DECREF(pyWsgiInputStream);
+ }
+ {
+ // wsgi.errors
+ XBMCAddon::xbmcwsgi::WsgiErrorStream* wsgiErrorStream = new XBMCAddon::xbmcwsgi::WsgiErrorStream();
+ if (request != NULL)
+ wsgiErrorStream->SetRequest(request);
+
+ PythonBindings::prepareForReturn(wsgiErrorStream);
+ PyObject* pyWsgiErrorStream = PythonBindings::makePythonInstance(wsgiErrorStream, false);
+ PyDict_SetItemString(pyEnviron, "wsgi.errors", pyWsgiErrorStream);
+ Py_DECREF(pyWsgiErrorStream);
+ }
+ {
+ // wsgi.multithread
+ PyObject* pyValue = Py_BuildValue("b", false);
+ PyDict_SetItemString(pyEnviron, "wsgi.multithread", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.multiprocess
+ PyObject* pyValue = Py_BuildValue("b", false);
+ PyDict_SetItemString(pyEnviron, "wsgi.multiprocess", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.run_once
+ PyObject* pyValue = Py_BuildValue("b", true);
+ PyDict_SetItemString(pyEnviron, "wsgi.run_once", pyValue);
+ Py_DECREF(pyValue);
+ }
+}
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h
new file mode 100644
index 0000000..3ec34c0
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/python/PythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+
+#include <map>
+#include <string>
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ class WsgiResponse;
+ }
+}
+
+class CHTTPPythonWsgiInvoker : public CHTTPPythonInvoker
+{
+public:
+ CHTTPPythonWsgiInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request);
+ ~CHTTPPythonWsgiInvoker() override;
+
+ // implementations of CHTTPPythonInvoker
+ HTTPPythonRequest* GetRequest() override;
+
+protected:
+ // overrides of CPythonInvoker
+ void executeScript(FILE* fp, const std::string& script, PyObject* moduleDict) override;
+ std::map<std::string, PythonModuleInitialization> getModules() const override;
+ const char* getInitializationScript() const override;
+
+private:
+ static std::map<std::string, std::string> createCgiEnvironment(
+ const HTTPPythonRequest* httpRequest, const ADDON::AddonPtr& addon);
+ static void addWsgiEnvironment(HTTPPythonRequest* request, void* environment);
+
+ XBMCAddon::xbmcwsgi::WsgiResponse* m_wsgiResponse;
+};
diff --git a/xbmc/network/mdns/CMakeLists.txt b/xbmc/network/mdns/CMakeLists.txt
new file mode 100644
index 0000000..e0b084a
--- /dev/null
+++ b/xbmc/network/mdns/CMakeLists.txt
@@ -0,0 +1,9 @@
+if(MDNS_FOUND)
+ set(SOURCES ZeroconfBrowserMDNS.cpp
+ ZeroconfMDNS.cpp)
+
+ set(HEADERS ZeroconfBrowserMDNS.h
+ ZeroconfMDNS.h)
+
+ core_add_library(network_mdns)
+endif()
diff --git a/xbmc/network/mdns/ZeroconfBrowserMDNS.cpp b/xbmc/network/mdns/ZeroconfBrowserMDNS.cpp
new file mode 100644
index 0000000..c4a1c1e
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfBrowserMDNS.cpp
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ZeroconfBrowserMDNS.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "network/DNSNameCache.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/WIN32Util.h"
+#endif //TARGET_WINDOWS
+
+using namespace std::chrono_literals;
+
+extern HWND g_hWnd;
+
+CZeroconfBrowserMDNS::CZeroconfBrowserMDNS()
+{
+ m_browser = NULL;
+}
+
+CZeroconfBrowserMDNS::~CZeroconfBrowserMDNS()
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ //make sure there are no browsers anymore
+ for (const auto& it : m_service_browsers)
+ doRemoveServiceType(it.first);
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_browser ), g_hWnd, BONJOUR_BROWSER_EVENT, 0 );
+#elif defined(TARGET_WINDOWS_STORE)
+ // need to modify this code to use WSAEventSelect since WSAAsyncSelect is not supported
+ CLog::Log(LOGDEBUG, "{} is not implemented for TARGET_WINDOWS_STORE", __FUNCTION__);
+#endif //TARGET_WINDOWS
+
+ if (m_browser)
+ DNSServiceRefDeallocate(m_browser);
+ m_browser = NULL;
+}
+
+void DNSSD_API CZeroconfBrowserMDNS::BrowserCallback(DNSServiceRef browser,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *serviceName,
+ const char *regtype,
+ const char *replyDomain,
+ void *context)
+{
+
+ if (errorCode == kDNSServiceErr_NoError)
+ {
+ //get our instance
+ CZeroconfBrowserMDNS* p_this = reinterpret_cast<CZeroconfBrowserMDNS*>(context);
+ //store the service
+ ZeroconfService s(serviceName, regtype, replyDomain);
+
+ if (flags & kDNSServiceFlagsAdd)
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "ZeroconfBrowserMDNS::BrowserCallback found service named: {}, type: {}, domain: {}",
+ s.GetName(), s.GetType(), s.GetDomain());
+ p_this->addDiscoveredService(browser, s);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG,
+ "ZeroconfBrowserMDNS::BrowserCallback service named: {}, type: {}, domain: {} "
+ "disappeared",
+ s.GetName(), s.GetType(), s.GetDomain());
+ p_this->removeDiscoveredService(browser, s);
+ }
+ if(! (flags & kDNSServiceFlagsMoreComing) )
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("zeroconf://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ CLog::Log(LOGDEBUG, "ZeroconfBrowserMDNS::BrowserCallback sent gui update for path zeroconf://");
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS::BrowserCallback returned (error = {})",
+ (int)errorCode);
+ }
+}
+
+void DNSSD_API CZeroconfBrowserMDNS::GetAddrInfoCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *hostname,
+ const struct sockaddr *address,
+ uint32_t ttl,
+ void *context
+ )
+{
+
+ if (errorCode)
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: GetAddrInfoCallback failed with error = {}",
+ (int)errorCode);
+ return;
+ }
+
+ std::string strIP;
+ CZeroconfBrowserMDNS* p_instance = static_cast<CZeroconfBrowserMDNS*> ( context );
+
+ if (address->sa_family == AF_INET)
+ strIP = inet_ntoa(((const struct sockaddr_in *)address)->sin_addr);
+
+ p_instance->m_resolving_service.SetIP(strIP);
+ p_instance->m_addrinfo_event.Set();
+}
+
+void DNSSD_API CZeroconfBrowserMDNS::ResolveCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *fullname,
+ const char *hosttarget,
+ uint16_t port, /* In network byte order */
+ uint16_t txtLen,
+ const unsigned char *txtRecord,
+ void *context
+ )
+{
+
+ if (errorCode)
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: ResolveCallback failed with error = {}",
+ (int)errorCode);
+ return;
+ }
+
+ DNSServiceErrorType err;
+ CZeroconfBrowser::ZeroconfService::tTxtRecordMap recordMap;
+ std::string strIP;
+ CZeroconfBrowserMDNS* p_instance = static_cast<CZeroconfBrowserMDNS*> ( context );
+
+ p_instance->m_resolving_service.SetHostname(hosttarget);
+
+ for(uint16_t i = 0; i < TXTRecordGetCount(txtLen, txtRecord); ++i)
+ {
+ char key[256];
+ uint8_t valueLen;
+ const void *value;
+ std::string strvalue;
+ err = TXTRecordGetItemAtIndex(txtLen, txtRecord,i ,sizeof(key) , key, &valueLen, &value);
+ if(err != kDNSServiceErr_NoError)
+ continue;
+
+ if(value != NULL && valueLen > 0)
+ strvalue.append((const char *)value, valueLen);
+
+ recordMap.insert(std::make_pair(key, strvalue));
+ }
+ p_instance->m_resolving_service.SetTxtRecords(recordMap);
+ p_instance->m_resolving_service.SetPort(ntohs(port));
+ p_instance->m_resolved_event.Set();
+}
+
+/// adds the service to list of found services
+void CZeroconfBrowserMDNS::addDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service)
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tDiscoveredServicesMap::iterator browserIt = m_discovered_services.find(browser);
+ if(browserIt == m_discovered_services.end())
+ {
+ //first service by this browser
+ browserIt = m_discovered_services.insert(make_pair(browser, std::vector<std::pair<ZeroconfService, unsigned int> >())).first;
+ }
+ //search this service
+ std::vector<std::pair<ZeroconfService, unsigned int> >& services = browserIt->second;
+ std::vector<std::pair<ZeroconfService, unsigned int> >::iterator serviceIt = services.begin();
+ for( ; serviceIt != services.end(); ++serviceIt)
+ {
+ if(serviceIt->first == fcr_service)
+ break;
+ }
+ if(serviceIt == services.end())
+ services.push_back(std::make_pair(fcr_service, 1));
+ else
+ ++serviceIt->second;
+}
+
+void CZeroconfBrowserMDNS::removeDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service)
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tDiscoveredServicesMap::iterator browserIt = m_discovered_services.find(browser);
+ //search this service
+ std::vector<std::pair<ZeroconfService, unsigned int> >& services = browserIt->second;
+ std::vector<std::pair<ZeroconfService, unsigned int> >::iterator serviceIt = services.begin();
+ for( ; serviceIt != services.end(); ++serviceIt)
+ if(serviceIt->first == fcr_service)
+ break;
+ if(serviceIt != services.end())
+ {
+ //decrease refCount
+ --serviceIt->second;
+ if(!serviceIt->second)
+ {
+ //eventually remove the service
+ services.erase(serviceIt);
+ }
+ } else
+ {
+ //looks like we missed the announce, no problem though..
+ }
+}
+
+
+bool CZeroconfBrowserMDNS::doAddServiceType(const std::string& fcr_service_type)
+{
+ DNSServiceErrorType err;
+ DNSServiceRef browser = NULL;
+
+#if !defined(HAS_MDNS_EMBEDDED)
+ if(m_browser == NULL)
+ {
+ err = DNSServiceCreateConnection(&m_browser);
+ if (err != kDNSServiceErr_NoError)
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceCreateConnection failed with error = {}",
+ (int)err);
+ return false;
+ }
+#if defined(TARGET_WINDOWS_DESKTOP)
+ err = WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_browser ), g_hWnd, BONJOUR_BROWSER_EVENT, FD_READ | FD_CLOSE );
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: WSAAsyncSelect failed with error = {}", (int)err);
+#elif defined(TARGET_WINDOWS_STORE)
+ // need to modify this code to use WSAEventSelect since WSAAsyncSelect is not supported
+ CLog::Log(LOGERROR, "{} is not implemented for TARGET_WINDOWS_STORE", __FUNCTION__);
+#endif // TARGET_WINDOWS_STORE
+ }
+#endif //!HAS_MDNS_EMBEDDED
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ browser = m_browser;
+ err = DNSServiceBrowse(&browser, kDNSServiceFlagsShareConnection, kDNSServiceInterfaceIndexAny, fcr_service_type.c_str(), NULL, BrowserCallback, this);
+ }
+
+ if( err != kDNSServiceErr_NoError )
+ {
+ if (browser)
+ DNSServiceRefDeallocate(browser);
+
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceBrowse returned (error = {})", (int)err);
+ return false;
+ }
+
+ //store the browser
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ m_service_browsers.insert(std::make_pair(fcr_service_type, browser));
+ }
+
+ return true;
+}
+
+bool CZeroconfBrowserMDNS::doRemoveServiceType(const std::string& fcr_service_type)
+{
+ //search for this browser and remove it from the map
+ DNSServiceRef browser = 0;
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tBrowserMap::iterator it = m_service_browsers.find(fcr_service_type);
+ if(it == m_service_browsers.end())
+ {
+ return false;
+ }
+ browser = it->second;
+ m_service_browsers.erase(it);
+ }
+
+ //remove the services of this browser
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tDiscoveredServicesMap::iterator it = m_discovered_services.find(browser);
+ if(it != m_discovered_services.end())
+ m_discovered_services.erase(it);
+ }
+
+ if (browser)
+ DNSServiceRefDeallocate(browser);
+
+ return true;
+}
+
+std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowserMDNS::doGetFoundServices()
+{
+ std::vector<CZeroconfBrowser::ZeroconfService> ret;
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ for (const auto& it : m_discovered_services)
+ {
+ auto& services = it.second;
+ for(unsigned int i = 0; i < services.size(); ++i)
+ {
+ ret.push_back(services[i].first);
+ }
+ }
+ return ret;
+}
+
+bool CZeroconfBrowserMDNS::doResolveService(CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout)
+{
+ DNSServiceErrorType err;
+ DNSServiceRef sdRef = NULL;
+
+ //start resolving
+ m_resolving_service = fr_service;
+ m_resolved_event.Reset();
+
+ err = DNSServiceResolve(&sdRef, 0, kDNSServiceInterfaceIndexAny, fr_service.GetName().c_str(), fr_service.GetType().c_str(), fr_service.GetDomain().c_str(), ResolveCallback, this);
+
+ if( err != kDNSServiceErr_NoError )
+ {
+ if (sdRef)
+ DNSServiceRefDeallocate(sdRef);
+
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceResolve returned (error = {})", (int)err);
+ return false;
+ }
+
+ err = DNSServiceProcessResult(sdRef);
+
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR,
+ "ZeroconfBrowserMDNS::doResolveService DNSServiceProcessResult returned (error = {})",
+ (int)err);
+
+#if defined(HAS_MDNS_EMBEDDED)
+ // when using the embedded mdns service the call to DNSServiceProcessResult
+ // above will not block until the resolving was finished - instead we have to
+ // wait for resolve to return or timeout
+ m_resolved_event.Wait(std::chrono::duration<double, std::milli>(f_timeout * 1000));
+#endif //HAS_MDNS_EMBEDDED
+ fr_service = m_resolving_service;
+
+ if (sdRef)
+ DNSServiceRefDeallocate(sdRef);
+
+ // resolve the hostname
+ if (!fr_service.GetHostname().empty())
+ {
+ std::string strIP;
+
+ // use mdns resolving
+ m_addrinfo_event.Reset();
+ sdRef = NULL;
+
+ err = DNSServiceGetAddrInfo(&sdRef, 0, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4, fr_service.GetHostname().c_str(), GetAddrInfoCallback, this);
+
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceGetAddrInfo returned (error = {})",
+ (int)err);
+
+ err = DNSServiceProcessResult(sdRef);
+
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(
+ LOGERROR,
+ "ZeroconfBrowserMDNS::doResolveService DNSServiceProcessResult returned (error = {})",
+ (int)err);
+
+#if defined(HAS_MDNS_EMBEDDED)
+ // when using the embedded mdns service the call to DNSServiceProcessResult
+ // above will not block until the resolving was finished - instead we have to
+ // wait for resolve to return or timeout
+ // give it 2 secs for resolving (resolving in mdns is cached and queued
+ // in timeslices off 1 sec
+ m_addrinfo_event.Wait(2000ms);
+#endif //HAS_MDNS_EMBEDDED
+ fr_service = m_resolving_service;
+
+ if (sdRef)
+ DNSServiceRefDeallocate(sdRef);
+
+ // fall back to our resolver
+ if (fr_service.GetIP().empty())
+ {
+ CLog::Log(LOGWARNING,
+ "ZeroconfBrowserMDNS: Could not resolve hostname {} falling back to CDNSNameCache",
+ fr_service.GetHostname());
+ if (CDNSNameCache::Lookup(fr_service.GetHostname(), strIP))
+ fr_service.SetIP(strIP);
+ else
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: Could not resolve hostname {}",
+ fr_service.GetHostname());
+ }
+ }
+
+ return (!fr_service.GetIP().empty());
+}
+
+void CZeroconfBrowserMDNS::ProcessResults()
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ DNSServiceErrorType err = DNSServiceProcessResult(m_browser);
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfWIN: DNSServiceProcessResult returned (error = {})", (int)err);
+}
diff --git a/xbmc/network/mdns/ZeroconfBrowserMDNS.h b/xbmc/network/mdns/ZeroconfBrowserMDNS.h
new file mode 100644
index 0000000..4ef3aac
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfBrowserMDNS.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/ZeroconfBrowser.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include <dns_sd.h>
+
+//platform specific implementation of zeroconfbrowser interface using native os x APIs
+class CZeroconfBrowserMDNS : public CZeroconfBrowser
+{
+public:
+ CZeroconfBrowserMDNS();
+ ~CZeroconfBrowserMDNS();
+
+private:
+ ///implementation if CZeroconfBrowser interface
+ ///@{
+ virtual bool doAddServiceType(const std::string& fcr_service_type);
+ virtual bool doRemoveServiceType(const std::string& fcr_service_type);
+
+ virtual std::vector<CZeroconfBrowser::ZeroconfService> doGetFoundServices();
+ virtual bool doResolveService(CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout);
+ ///@}
+
+ /// browser callback
+ static void DNSSD_API BrowserCallback(DNSServiceRef browser,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *serviceName,
+ const char *regtype,
+ const char *replyDomain,
+ void *context);
+ /// GetAddrInfo callback
+ static void DNSSD_API GetAddrInfoCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *hostname,
+ const struct sockaddr *address,
+ uint32_t ttl,
+ void *context
+ );
+
+ /// resolve callback
+ static void DNSSD_API ResolveCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *fullname,
+ const char *hosttarget,
+ uint16_t port, /* In network byte order */
+ uint16_t txtLen,
+ const unsigned char *txtRecord,
+ void *context
+ );
+
+ /// adds the service to list of found services
+ void addDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service);
+ /// removes the service from list of found services
+ void removeDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service);
+ // win32: process replies from the bonjour daemon
+ void ProcessResults();
+
+ //shared variables (with guard)
+ CCriticalSection m_data_guard;
+ // tBrowserMap maps service types the corresponding browser
+ typedef std::map<std::string, DNSServiceRef> tBrowserMap;
+ tBrowserMap m_service_browsers;
+ //tDiscoveredServicesMap maps browsers to their discovered services + a ref-count for each service
+ //ref-count is needed, because a service might pop up more than once, if there's more than one network-iface
+ typedef std::map<DNSServiceRef, std::vector<std::pair<ZeroconfService, unsigned int> > > tDiscoveredServicesMap;
+ tDiscoveredServicesMap m_discovered_services;
+ DNSServiceRef m_browser;
+ CZeroconfBrowser::ZeroconfService m_resolving_service;
+ CEvent m_resolved_event;
+ CEvent m_addrinfo_event;
+};
diff --git a/xbmc/network/mdns/ZeroconfMDNS.cpp b/xbmc/network/mdns/ZeroconfMDNS.cpp
new file mode 100644
index 0000000..5c65d9f
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfMDNS.cpp
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ZeroconfMDNS.h"
+
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <sstream>
+#include <string>
+
+#include <arpa/inet.h>
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/WIN32Util.h"
+#endif //TARGET_WINDOWS
+
+#if defined(HAS_MDNS_EMBEDDED)
+#include <mDnsEmbedded.h>
+#endif //HAS_MDNS_EMBEDDED
+
+extern HWND g_hWnd;
+
+void CZeroconfMDNS::Process()
+{
+#if defined(HAS_MDNS_EMBEDDED)
+ CLog::Log(LOGDEBUG, "ZeroconfEmbedded - processing...");
+ struct timeval timeout;
+ timeout.tv_sec = 1;
+ while (( !m_bStop ))
+ embedded_mDNSmainLoop(timeout);
+#endif //HAS_MDNS_EMBEDDED
+
+}
+
+
+CZeroconfMDNS::CZeroconfMDNS() : CThread("ZeroconfEmbedded")
+{
+ m_service = NULL;
+#if defined(HAS_MDNS_EMBEDDED)
+ embedded_mDNSInit();
+ Create();
+#endif //HAS_MDNS_EMBEDDED
+}
+
+CZeroconfMDNS::~CZeroconfMDNS()
+{
+ doStop();
+#if defined(HAS_MDNS_EMBEDDED)
+ StopThread();
+ embedded_mDNSExit();
+#endif //HAS_MDNS_EMBEDDED
+}
+
+bool CZeroconfMDNS::IsZCdaemonRunning()
+{
+#if !defined(HAS_MDNS_EMBEDDED)
+ uint32_t version;
+ uint32_t size = sizeof(version);
+ DNSServiceErrorType err = DNSServiceGetProperty(kDNSServiceProperty_DaemonVersion, &version, &size);
+ if(err != kDNSServiceErr_NoError)
+ {
+ CLog::Log(LOGERROR, "ZeroconfMDNS: Zeroconf can't be started probably because Apple's Bonjour Service isn't installed. You can get it by either installing Itunes or Apple's Bonjour Print Service for Windows (http://support.apple.com/kb/DL999)");
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(34300), g_localizeStrings.Get(34301), 10000, true);
+ return false;
+ }
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS:Bonjour version is {}.{}", version / 10000,
+ version / 100 % 100);
+#endif //!HAS_MDNS_EMBEDDED
+ return true;
+}
+
+//methods to implement for concrete implementations
+bool CZeroconfMDNS::doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt)
+{
+ DNSServiceRef netService = NULL;
+ TXTRecordRef txtRecord;
+ DNSServiceErrorType err;
+ TXTRecordCreate(&txtRecord, 0, NULL);
+
+#if !defined(HAS_MDNS_EMBEDDED)
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ if(m_service == NULL)
+ {
+ err = DNSServiceCreateConnection(&m_service);
+ if (err != kDNSServiceErr_NoError)
+ {
+ CLog::Log(LOGERROR, "ZeroconfMDNS: DNSServiceCreateConnection failed with error = {}",
+ (int)err);
+ return false;
+ }
+#ifdef TARGET_WINDOWS_STORE
+ CLog::Log(LOGERROR, "ZeroconfMDNS: WSAAsyncSelect not yet supported for TARGET_WINDOWS_STORE");
+#else
+ err = WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_service ), g_hWnd, BONJOUR_EVENT, FD_READ | FD_CLOSE );
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfMDNS: WSAAsyncSelect failed with error = {}", (int)err);
+#endif
+ }
+#endif //!HAS_MDNS_EMBEDDED
+
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: identifier: {} type: {} name:{} port:{}", fcr_identifier,
+ fcr_type, fcr_name, f_port);
+
+ //add txt records
+ if(!txt.empty())
+ {
+ for (const auto& it : txt)
+ {
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: key:{}, value:{}", it.first, it.second);
+ uint8_t txtLen = (uint8_t)strlen(it.second.c_str());
+ TXTRecordSetValue(&txtRecord, it.first.c_str(), txtLen, it.second.c_str());
+ }
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ netService = m_service;
+ err = DNSServiceRegister(&netService, kDNSServiceFlagsShareConnection, 0, fcr_name.c_str(), fcr_type.c_str(), NULL, NULL, htons(f_port), TXTRecordGetLength(&txtRecord), TXTRecordGetBytesPtr(&txtRecord), registerCallback, NULL);
+ }
+
+ if (err != kDNSServiceErr_NoError)
+ {
+ // Something went wrong so lets clean up.
+ if (netService)
+ DNSServiceRefDeallocate(netService);
+
+ CLog::Log(LOGERROR, "ZeroconfMDNS: DNSServiceRegister returned (error = {})", (int)err);
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ struct tServiceRef newService;
+ newService.serviceRef = netService;
+ newService.txtRecordRef = txtRecord;
+ newService.updateNumber = 0;
+ m_services.insert(make_pair(fcr_identifier, newService));
+ }
+
+ return err == kDNSServiceErr_NoError;
+}
+
+bool CZeroconfMDNS::doForceReAnnounceService(const std::string& fcr_identifier)
+{
+ bool ret = false;
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tServiceMap::iterator it = m_services.find(fcr_identifier);
+ if(it != m_services.end())
+ {
+ // for force announcing a service with mdns we need
+ // to change a txt record - so we diddle between
+ // even and odd dummy records here
+ if ( (it->second.updateNumber % 2) == 0)
+ TXTRecordSetValue(&it->second.txtRecordRef, "xbmcdummy", strlen("evendummy"), "evendummy");
+ else
+ TXTRecordSetValue(&it->second.txtRecordRef, "xbmcdummy", strlen("odddummy"), "odddummy");
+ it->second.updateNumber++;
+
+ if (DNSServiceUpdateRecord(it->second.serviceRef, NULL, 0, TXTRecordGetLength(&it->second.txtRecordRef), TXTRecordGetBytesPtr(&it->second.txtRecordRef), 0) == kDNSServiceErr_NoError)
+ ret = true;
+ }
+ return ret;
+}
+
+bool CZeroconfMDNS::doRemoveService(const std::string& fcr_ident)
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tServiceMap::iterator it = m_services.find(fcr_ident);
+ if(it != m_services.end())
+ {
+ DNSServiceRefDeallocate(it->second.serviceRef);
+ TXTRecordDeallocate(&it->second.txtRecordRef);
+ m_services.erase(it);
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: Removed service {}", fcr_ident);
+ return true;
+ }
+ else
+ return false;
+}
+
+void CZeroconfMDNS::doStop()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: Shutdown services");
+ for (auto& it : m_services)
+ {
+ DNSServiceRefDeallocate(it.second.serviceRef);
+ TXTRecordDeallocate(&it.second.txtRecordRef);
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: Removed service {}", it.first);
+ }
+ m_services.clear();
+ }
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+#if defined(TARGET_WINDOWS_STORE)
+ CLog::Log(LOGERROR, "ZeroconfMDNS: WSAAsyncSelect not yet supported for TARGET_WINDOWS_STORE");
+#else
+ WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_service ), g_hWnd, BONJOUR_EVENT, 0 );
+#endif //TARGET_WINDOWS
+
+ if (m_service)
+ DNSServiceRefDeallocate(m_service);
+ m_service = NULL;
+ }
+}
+
+void DNSSD_API CZeroconfMDNS::registerCallback(DNSServiceRef sdref, const DNSServiceFlags flags, DNSServiceErrorType errorCode, const char *name, const char *regtype, const char *domain, void *context)
+{
+ (void)sdref; // Unused
+ (void)flags; // Unused
+ (void)context; // Unused
+
+ if (errorCode == kDNSServiceErr_NoError)
+ {
+ if (flags & kDNSServiceFlagsAdd)
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} now registered and active", name, regtype, domain);
+ else
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} registration removed", name, regtype, domain);
+ }
+ else if (errorCode == kDNSServiceErr_NameConflict)
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} Name in use, please choose another", name, regtype,
+ domain);
+ else
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} error code {}", name, regtype, domain, errorCode);
+}
+
+void CZeroconfMDNS::ProcessResults()
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ DNSServiceErrorType err = DNSServiceProcessResult(m_service);
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfMDNS: DNSServiceProcessResult returned (error = {})", (int)err);
+}
+
diff --git a/xbmc/network/mdns/ZeroconfMDNS.h b/xbmc/network/mdns/ZeroconfMDNS.h
new file mode 100644
index 0000000..95959a1
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfMDNS.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "network/Zeroconf.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <dns_sd.h>
+
+class CZeroconfMDNS : public CZeroconf,public CThread
+{
+public:
+ CZeroconfMDNS();
+ ~CZeroconfMDNS();
+
+protected:
+
+ //CThread interface
+ void Process();
+
+ //implement base CZeroConf interface
+ bool doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt);
+
+ bool doForceReAnnounceService(const std::string& fcr_identifier);
+ bool doRemoveService(const std::string& fcr_ident);
+
+ virtual void doStop();
+
+ bool IsZCdaemonRunning();
+
+ void ProcessResults();
+
+private:
+
+ static void DNSSD_API registerCallback(DNSServiceRef sdref,
+ const DNSServiceFlags flags,
+ DNSServiceErrorType errorCode,
+ const char *name,
+ const char *regtype,
+ const char *domain,
+ void *context);
+
+
+ //lock + data (accessed from runloop(main thread) + the rest)
+ CCriticalSection m_data_guard;
+ struct tServiceRef
+ {
+ DNSServiceRef serviceRef;
+ TXTRecordRef txtRecordRef;
+ int updateNumber;
+ };
+ typedef std::map<std::string, struct tServiceRef> tServiceMap;
+ tServiceMap m_services;
+ DNSServiceRef m_service;
+};
diff --git a/xbmc/network/test/CMakeLists.txt b/xbmc/network/test/CMakeLists.txt
new file mode 100644
index 0000000..a323d18
--- /dev/null
+++ b/xbmc/network/test/CMakeLists.txt
@@ -0,0 +1,5 @@
+if(MICROHTTPD_FOUND)
+ set(SOURCES TestWebServer.cpp)
+
+ core_add_test_library(network_test)
+endif()
diff --git a/xbmc/network/test/TestWebServer.cpp b/xbmc/network/test/TestWebServer.cpp
new file mode 100644
index 0000000..aa728ec
--- /dev/null
+++ b/xbmc/network/test/TestWebServer.cpp
@@ -0,0 +1,926 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include <gtest/gtest.h>
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/File.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPVfsHandler.h"
+#include "network/httprequesthandler/HTTPJsonRpcHandler.h"
+#include "settings/MediaSourceSettings.h"
+#include "test/TestUtils.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <random>
+
+using namespace XFILE;
+
+#define WEBSERVER_HOST "localhost"
+
+#define TEST_URL_JSONRPC "jsonrpc"
+
+#define TEST_FILES_DATA "test"
+#define TEST_FILES_DATA_RANGES "range1;range2;range3"
+#define TEST_FILES_HTML TEST_FILES_DATA ".html"
+#define TEST_FILES_RANGES TEST_FILES_DATA "-ranges.txt"
+
+class TestWebServer : public testing::Test
+{
+protected:
+ TestWebServer()
+ : webserver(),
+ sourcePath(XBMC_REF_FILE_PATH("xbmc/network/test/data/webserver/"))
+ {
+ static uint16_t port;
+ if (port == 0)
+ {
+ std::random_device rd;
+ std::mt19937 mt(rd());
+ std::uniform_int_distribution<uint16_t> dist(49152, 65535);
+ port = dist(mt);
+ }
+ webserverPort = port;
+ baseUrl = StringUtils::Format("http://" WEBSERVER_HOST ":{}", webserverPort);
+ }
+ ~TestWebServer() override = default;
+
+protected:
+ void SetUp() override
+ {
+ SetupMediaSources();
+
+ webserver.Start(webserverPort, "", "");
+ webserver.RegisterRequestHandler(&m_jsonRpcHandler);
+ webserver.RegisterRequestHandler(&m_vfsHandler);
+ }
+
+ void TearDown() override
+ {
+ if (webserver.IsStarted())
+ webserver.Stop();
+
+ webserver.UnregisterRequestHandler(&m_vfsHandler);
+ webserver.UnregisterRequestHandler(&m_jsonRpcHandler);
+
+ TearDownMediaSources();
+ }
+
+ void SetupMediaSources()
+ {
+ CMediaSource source;
+ source.strName = "WebServer Share";
+ source.strPath = sourcePath;
+ source.vecPaths.push_back(sourcePath);
+ source.m_allowSharing = true;
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ source.m_iLockMode = LOCK_MODE_EVERYONE;
+ source.m_ignore = true;
+
+ CMediaSourceSettings::GetInstance().AddShare("videos", source);
+ }
+
+ void TearDownMediaSources()
+ {
+ CMediaSourceSettings::GetInstance().Clear();
+ }
+
+ std::string GetUrl(const std::string& path)
+ {
+ if (path.empty())
+ return baseUrl;
+
+ return URIUtils::AddFileToFolder(baseUrl, path);
+ }
+
+ std::string GetUrlOfTestFile(const std::string& testFile)
+ {
+ if (testFile.empty())
+ return "";
+
+ std::string path = URIUtils::AddFileToFolder(sourcePath, testFile);
+ path = CURL::Encode(path);
+ path = URIUtils::AddFileToFolder("vfs", path);
+
+ return GetUrl(path);
+ }
+
+ bool GetLastModifiedOfTestFile(const std::string& testFile, CDateTime& lastModified)
+ {
+ CFile file;
+ if (!file.Open(URIUtils::AddFileToFolder(sourcePath, testFile), READ_NO_CACHE))
+ return false;
+
+ struct __stat64 statBuffer;
+ if (file.Stat(&statBuffer) != 0)
+ return false;
+
+ struct tm *time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer.st_mtime);
+#endif
+ if (time == NULL)
+ return false;
+
+ lastModified = *time;
+ return lastModified.IsValid();
+ }
+
+ 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());
+ // Must be only one "Content-Length" header
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // 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);
+ }
+
+ void CheckRangesTestFileResponse(const CCurlFile& curl, int httpStatus = MHD_HTTP_OK, bool empty = false)
+ {
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Only zero or one "Content-Length" headers
+ ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+
+ // check the protocol line for the expected HTTP status
+ std::string httpStatusString = StringUtils::Format(" {} ", 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)
+ {
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ 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);
+ }
+
+ void CheckRangesTestFileResponse(const CCurlFile& curl, const std::string& result, const CHttpRanges& ranges)
+ {
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Only zero or one "Content-Length" headers
+ ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+
+ // check the protocol line for the expected HTTP status
+ std::string httpStatusString = StringUtils::Format(" {} ", 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())
+ {
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ 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(static_cast<size_t>(firstRange.GetFirstPosition()), static_cast<size_t>(firstRange.GetLength()));
+ EXPECT_STREQ(expectedContent.c_str(), result.c_str());
+
+ // and Content-Length
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ EXPECT_STREQ(std::to_string(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(0U, 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(0U, 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(2U, 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(0U, 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(2U, 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(static_cast<size_t>(range.GetFirstPosition()), static_cast<size_t>(range.GetLength())).c_str(), data.c_str());
+ }
+ }
+
+ std::string GenerateRangeHeaderValue(unsigned int start, unsigned int end)
+ {
+ return StringUtils::Format("bytes={}-{}", start, end);
+ }
+
+ CWebServer webserver;
+ CHTTPJsonRpcHandler m_jsonRpcHandler;
+ CHTTPVfsHandler m_vfsHandler;
+ std::string baseUrl;
+ std::string sourcePath;
+ uint16_t webserverPort;
+};
+
+TEST_F(TestWebServer, IsStarted)
+{
+ ASSERT_TRUE(webserver.IsStarted());
+}
+
+TEST_F(TestWebServer, CanGetJsonRpcApiDescriptionWithHttpGet)
+{
+ std::string result;
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC), result));
+ ASSERT_FALSE(result.empty());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).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);
+}
+
+TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpGet)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }")), result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).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);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CannotModifyOverJsonRpcWithHttpGet)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }")), result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+ // it must contain the "error" property with the "Bad client permission" error code
+ ASSERT_TRUE(resultObj.isMember("error") && resultObj["error"].isObject());
+ ASSERT_TRUE(resultObj["error"].isMember("code") && resultObj["error"]["code"].isInteger());
+ ASSERT_EQ(JSONRPC::BadPermission, resultObj["error"]["code"].asInteger());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).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);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpPost)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ curl.SetMimeType("application/json");
+ ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }", result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).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);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CanModifyOverJsonRpcWithHttpPost)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ curl.SetMimeType("application/json");
+ ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }", result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+ // it must contain the "result" property with the "OK" value
+ ASSERT_TRUE(resultObj.isMember("result") && resultObj["result"].isString());
+ EXPECT_STREQ("OK", resultObj["result"].asString().c_str());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).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);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CanNotHeadNonExistingFile)
+{
+ CCurlFile curl;
+ ASSERT_FALSE(curl.Exists(CURL(GetUrlOfTestFile("file_does_not_exist"))));
+}
+
+TEST_F(TestWebServer, CanHeadFile)
+{
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Exists(CURL(GetUrlOfTestFile(TEST_FILES_HTML))));
+
+ CheckHtmlTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanNotGetNonExistingFile)
+{
+ std::string result;
+ CCurlFile curl;
+ ASSERT_FALSE(curl.Get(GetUrlOfTestFile("file_does_not_exist"), result));
+ ASSERT_TRUE(result.empty());
+}
+
+TEST_F(TestWebServer, CanGetFile)
+{
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_HTML), result));
+ ASSERT_STREQ(TEST_FILES_DATA, result.c_str());
+
+ CheckHtmlTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetFileForcingNoCache)
+{
+ // check non-cacheable HTML with Control-Cache: no-cache
+ std::string result;
+ CCurlFile curl_html;
+ curl_html.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl_html.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
+ ASSERT_TRUE(curl_html.Get(GetUrlOfTestFile(TEST_FILES_HTML), result));
+ EXPECT_STREQ(TEST_FILES_DATA, result.c_str());
+ CheckHtmlTestFileResponse(curl_html);
+
+ // check cacheable text file with Control-Cache: no-cache
+ result.clear();
+ CCurlFile curl_txt;
+ curl_txt.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl_txt.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
+ ASSERT_TRUE(curl_txt.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl_txt);
+
+ // check cacheable text file with deprecated Pragma: no-cache
+ result.clear();
+ CCurlFile curl_txt_pragma;
+ curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_PRAGMA, "no-cache");
+ ASSERT_TRUE(curl_txt_pragma.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl_txt_pragma);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithOlderIfModifiedSince)
+{
+ // 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-Modified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedOlder.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, CanGetCachedFileWithNewerIfModifiedSince)
+{
+ // 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
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE,
+ lastModifiedNewer.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, 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, CanGetCachedFileWithNewerIfUnmodifiedSince)
+{
+ // 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-Unmodified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_)
+{
+ 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 whole file but specify the beginning of the range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_End)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size());
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify the whole range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_2xEnd)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size() * 2);
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_First)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = GenerateRangeHeaderValue(0, rangedContent.front().size() - 1);
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRangeFirst_Second)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = GenerateRangeHeaderValue(rangedContent.front().size() + 1, rangedContent.front().size() + 1 + rangedContent.at(2).size() - 1);
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange_Last)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range =
+ StringUtils::Format("bytes=-{}", static_cast<unsigned int>(rangedContent.back().size()));
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecond)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = StringUtils::Format(
+ "bytes=0-{},{}-{}", static_cast<unsigned int>(rangedContent.front().size() - 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1) +
+ static_cast<unsigned int>(rangedContent.at(1).size() - 1));
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecondLast)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = StringUtils::Format(
+ "bytes=0-{},{}-{},-{}", static_cast<unsigned int>(rangedContent.front().size() - 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1) +
+ static_cast<unsigned int>(rangedContent.at(1).size() - 1),
+ static_cast<unsigned int>(rangedContent.back().size()));
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetCachedRangedFileWithOlderIfRange)
+{
+ 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));
+ CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);
+
+ // 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, lastModifiedOlder.GetAsRFC1123DateTime());
+ 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);
+}
+
+TEST_F(TestWebServer, CanGetCachedRangedFileWithNewerIfRange)
+{
+ 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));
+ CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);
+
+ // 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, lastModifiedNewer.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
diff --git a/xbmc/network/test/data/webserver/test-ranges.txt b/xbmc/network/test/data/webserver/test-ranges.txt
new file mode 100644
index 0000000..6c0a04b
--- /dev/null
+++ b/xbmc/network/test/data/webserver/test-ranges.txt
@@ -0,0 +1 @@
+range1;range2;range3 \ No newline at end of file
diff --git a/xbmc/network/test/data/webserver/test.html b/xbmc/network/test/data/webserver/test.html
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/xbmc/network/test/data/webserver/test.html
@@ -0,0 +1 @@
+test \ No newline at end of file
diff --git a/xbmc/network/test/data/webserver/test.png b/xbmc/network/test/data/webserver/test.png
new file mode 100644
index 0000000..f792601
--- /dev/null
+++ b/xbmc/network/test/data/webserver/test.png
Binary files differ
diff --git a/xbmc/network/upnp/CMakeLists.txt b/xbmc/network/upnp/CMakeLists.txt
new file mode 100644
index 0000000..e558cfc
--- /dev/null
+++ b/xbmc/network/upnp/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES UPnP.cpp
+ UPnPInternal.cpp
+ UPnPPlayer.cpp
+ UPnPRenderer.cpp
+ UPnPServer.cpp
+ UPnPSettings.cpp)
+
+set(HEADERS UPnP.h
+ UPnPInternal.h
+ UPnPPlayer.h
+ UPnPRenderer.h
+ UPnPServer.h
+ UPnPSettings.h)
+
+core_add_library(network_upnp)
+if(ENABLE_STATIC_LIBS)
+ target_link_libraries(network_upnp PRIVATE upnp)
+endif()
diff --git a/xbmc/network/upnp/UPnP.cpp b/xbmc/network/upnp/UPnP.cpp
new file mode 100644
index 0000000..0e6b689
--- /dev/null
+++ b/xbmc/network/upnp/UPnP.cpp
@@ -0,0 +1,891 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "UPnP.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "UPnPInternal.h"
+#include "UPnPRenderer.h"
+#include "UPnPServer.h"
+#include "UPnPSettings.h"
+#include "URL.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <memory>
+#include <mutex>
+#include <set>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace UPNP;
+using namespace KODI::MESSAGING;
+
+#define UPNP_DEFAULT_MAX_RETURNED_ITEMS 200
+#define UPNP_DEFAULT_MIN_RETURNED_ITEMS 30
+
+/*
+# Play speed
+# 1 normal
+# 0 invalid
+DLNA_ORG_PS = 'DLNA.ORG_PS'
+DLNA_ORG_PS_VAL = '1'
+
+# Conversion Indicator
+# 1 transcoded
+# 0 not transcoded
+DLNA_ORG_CI = 'DLNA.ORG_CI'
+DLNA_ORG_CI_VAL = '0'
+
+# Operations
+# 00 not time seek range, not range
+# 01 range supported
+# 10 time seek range supported
+# 11 both supported
+DLNA_ORG_OP = 'DLNA.ORG_OP'
+DLNA_ORG_OP_VAL = '01'
+
+# Flags
+# senderPaced 80000000 31
+# lsopTimeBasedSeekSupported 40000000 30
+# lsopByteBasedSeekSupported 20000000 29
+# playcontainerSupported 10000000 28
+# s0IncreasingSupported 08000000 27
+# sNIncreasingSupported 04000000 26
+# rtspPauseSupported 02000000 25
+# streamingTransferModeSupported 01000000 24
+# interactiveTransferModeSupported 00800000 23
+# backgroundTransferModeSupported 00400000 22
+# connectionStallingSupported 00200000 21
+# dlnaVersion15Supported 00100000 20
+DLNA_ORG_FLAGS = 'DLNA.ORG_FLAGS'
+DLNA_ORG_FLAGS_VAL = '01500000000000000000000000000000'
+*/
+
+/*----------------------------------------------------------------------
+| NPT_Console::Output
++---------------------------------------------------------------------*/
+void
+NPT_Console::Output(const char* msg) { }
+
+spdlog::level::level_enum ConvertLogLevel(int nptLogLevel)
+{
+ if (nptLogLevel >= NPT_LOG_LEVEL_FATAL)
+ return spdlog::level::critical;
+ if (nptLogLevel >= NPT_LOG_LEVEL_SEVERE)
+ return spdlog::level::err;
+ if (nptLogLevel >= NPT_LOG_LEVEL_WARNING)
+ return spdlog::level::warn;
+ if (nptLogLevel >= NPT_LOG_LEVEL_FINE)
+ return spdlog::level::info;
+ if (nptLogLevel >= NPT_LOG_LEVEL_FINER)
+ return spdlog::level::debug;
+
+ return spdlog::level::trace;
+}
+
+void
+UPnPLogger(const NPT_LogRecord* record)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("Platinum");
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGUPNP))
+ logger->log(ConvertLogLevel(record->m_Level), "[{}]: {}", record->m_LoggerName,
+ record->m_Message);
+}
+
+namespace UPNP
+{
+
+/*----------------------------------------------------------------------
+| static
++---------------------------------------------------------------------*/
+CUPnP* CUPnP::upnp = NULL;
+static NPT_List<void*> g_UserData;
+static NPT_Mutex g_UserDataLock;
+
+/*----------------------------------------------------------------------
+| CDeviceHostReferenceHolder class
++---------------------------------------------------------------------*/
+class CDeviceHostReferenceHolder
+{
+public:
+ PLT_DeviceHostReference m_Device;
+};
+
+/*----------------------------------------------------------------------
+| CCtrlPointReferenceHolder class
++---------------------------------------------------------------------*/
+class CCtrlPointReferenceHolder
+{
+public:
+ PLT_CtrlPointReference m_CtrlPoint;
+};
+
+/*----------------------------------------------------------------------
+| CUPnPCleaner class
++---------------------------------------------------------------------*/
+class CUPnPCleaner : public NPT_Thread
+{
+public:
+ explicit CUPnPCleaner(CUPnP* upnp) : NPT_Thread(true), m_UPnP(upnp) {}
+ void Run() override {
+ delete m_UPnP;
+ }
+
+ CUPnP* m_UPnP;
+};
+
+/*----------------------------------------------------------------------
+| CMediaBrowser class
++---------------------------------------------------------------------*/
+class CMediaBrowser : public PLT_SyncMediaBrowser, public PLT_MediaContainerChangesListener
+{
+public:
+ explicit CMediaBrowser(PLT_CtrlPointReference& ctrlPoint)
+ : PLT_SyncMediaBrowser(ctrlPoint, true),
+ m_logger(CServiceBroker::GetLogging().GetLogger("UPNP::CMediaBrowser"))
+ {
+ SetContainerListener(this);
+ }
+
+ // PLT_MediaBrowser methods
+ bool OnMSAdded(PLT_DeviceDataReference& device) override
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("upnp://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+
+ return PLT_SyncMediaBrowser::OnMSAdded(device);
+ }
+ void OnMSRemoved(PLT_DeviceDataReference& device) override
+ {
+ PLT_SyncMediaBrowser::OnMSRemoved(device);
+
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("upnp://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+
+ PLT_SyncMediaBrowser::OnMSRemoved(device);
+ }
+
+ // PLT_MediaContainerChangesListener methods
+ void OnContainerChanged(PLT_DeviceDataReference& device,
+ const char* item_id,
+ const char* update_id) override
+ {
+ NPT_String path = "upnp://"+device->GetUUID()+"/";
+ if (!NPT_StringsEqual(item_id, "0")) {
+ std::string id(CURL::Encode(item_id));
+ URIUtils::AddSlashAtEnd(id);
+ path += id.c_str();
+ }
+
+ m_logger->debug("notified container update {}", (const char*)path);
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam(path.GetChars());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ bool MarkWatched(const CFileItem& item, const bool watched)
+ {
+ if (watched) {
+ CFileItem temp(item);
+ temp.SetProperty("original_listitem_url", item.GetPath());
+ return SaveFileState(temp, CBookmark(), watched);
+ }
+ else {
+ m_logger->debug("Marking video item {} as watched", item.GetPath());
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ values.insert(std::make_pair("<upnp:playCount>1</upnp:playCount>",
+ "<upnp:playCount>0</upnp:playCount>"));
+ return InvokeUpdateObject(item.GetPath().c_str(), values);
+ }
+ }
+
+ bool SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
+ {
+ std::string path = item.GetProperty("original_listitem_url").asString();
+ if (!item.HasVideoInfoTag() || path.empty()) {
+ return false;
+ }
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ if (item.GetVideoInfoTag()->GetResumePoint().timeInSeconds != bookmark.timeInSeconds) {
+ m_logger->debug("Updating resume point for item {}", path);
+ long time = (long)bookmark.timeInSeconds;
+ if (time < 0)
+ time = 0;
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ (long)item.GetVideoInfoTag()->GetResumePoint().timeInSeconds),
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ time)));
+
+ NPT_String curr_value = "<xbmc:lastPlayerState>";
+ PLT_Didl::AppendXmlEscape(curr_value, item.GetVideoInfoTag()->GetResumePoint().playerState.c_str());
+ curr_value += "</xbmc:lastPlayerState>";
+ NPT_String new_value = "<xbmc:lastPlayerState>";
+ PLT_Didl::AppendXmlEscape(new_value, bookmark.playerState.c_str());
+ new_value += "</xbmc:lastPlayerState>";
+ values.insert(std::make_pair(curr_value, new_value));
+ }
+ if (updatePlayCount) {
+ m_logger->debug("Marking video item {} as watched", path);
+ values.insert(std::make_pair("<upnp:playCount>0</upnp:playCount>",
+ "<upnp:playCount>1</upnp:playCount>"));
+ }
+
+ return InvokeUpdateObject(path.c_str(), values);
+ }
+
+ bool UpdateItem(const std::string& path, const CFileItem& item)
+ {
+ if (path.empty())
+ return false;
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ if (item.HasVideoInfoTag())
+ {
+ // handle playcount
+ const CVideoInfoTag *details = item.GetVideoInfoTag();
+ int playcountOld = 0, playcountNew = 0;
+ if (details->GetPlayCount() <= 0)
+ playcountOld = 1;
+ else
+ playcountNew = details->GetPlayCount();
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:playCount>%d</upnp:playCount>", playcountOld),
+ NPT_String::Format("<upnp:playCount>%d</upnp:playCount>", playcountNew)));
+
+ // handle lastplayed
+ CDateTime lastPlayedOld, lastPlayedNew;
+ if (!details->m_lastPlayed.IsValid())
+ lastPlayedOld = CDateTime::GetCurrentDateTime();
+ else
+ lastPlayedNew = details->m_lastPlayed;
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackTime>%s</upnp:lastPlaybackTime>",
+ lastPlayedOld.GetAsW3CDateTime().c_str()),
+ NPT_String::Format("<upnp:lastPlaybackTime>%s</upnp:lastPlaybackTime>",
+ lastPlayedNew.GetAsW3CDateTime().c_str())));
+
+ // handle resume point
+ long resumePointOld = 0L, resumePointNew = 0L;
+ if (details->GetResumePoint().timeInSeconds <= 0)
+ resumePointOld = 1;
+ else
+ resumePointNew = static_cast<long>(details->GetResumePoint().timeInSeconds);
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ resumePointOld),
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ resumePointNew)));
+ }
+
+ return InvokeUpdateObject(path.c_str(), values);
+ }
+
+ bool InvokeUpdateObject(const char *id, const std::set<std::pair<NPT_String, NPT_String> >& values)
+ {
+ CURL url(id);
+ PLT_DeviceDataReference device;
+ PLT_Service* cds;
+ PLT_ActionReference action;
+ NPT_String curr_value, new_value;
+
+ m_logger->debug("attempting to invoke UpdateObject for {}", id);
+
+ // check this server supports UpdateObject action
+ NPT_CHECK_LABEL(FindServer(url.GetHostName().c_str(), device),failed);
+ NPT_CHECK_LABEL(device->FindServiceById("urn:upnp-org:serviceId:ContentDirectory", cds),failed);
+
+ NPT_CHECK_LABEL(m_CtrlPoint->CreateAction(
+ device,
+ "urn:schemas-upnp-org:service:ContentDirectory:1",
+ "UpdateObject",
+ action), failed);
+
+ NPT_CHECK_LABEL(action->SetArgumentValue("ObjectID", url.GetFileName().c_str()), failed);
+
+ // put together the current and the new value string
+ for (std::set<std::pair<NPT_String, NPT_String> >::const_iterator value = values.begin(); value != values.end(); ++value)
+ {
+ if (!curr_value.IsEmpty())
+ curr_value.Append(",");
+ if (!new_value.IsEmpty())
+ new_value.Append(",");
+
+ curr_value.Append(value->first);
+ new_value.Append(value->second);
+ }
+ NPT_CHECK_LABEL(action->SetArgumentValue("CurrentTagValue", curr_value), failed);
+ NPT_CHECK_LABEL(action->SetArgumentValue("NewTagValue", new_value), failed);
+
+ NPT_CHECK_LABEL(m_CtrlPoint->InvokeAction(action, NULL),failed);
+
+ m_logger->debug("invoked UpdateObject successfully");
+ return true;
+
+ failed:
+ m_logger->info("invoking UpdateObject failed");
+ return false;
+ }
+
+ private:
+ Logger m_logger;
+};
+
+
+/*----------------------------------------------------------------------
+| CMediaController class
++---------------------------------------------------------------------*/
+class CMediaController
+ : public PLT_MediaControllerDelegate
+ , public PLT_MediaController
+{
+public:
+ explicit CMediaController(PLT_CtrlPointReference& ctrl_point)
+ : PLT_MediaController(ctrl_point)
+ {
+ PLT_MediaController::SetDelegate(this);
+ }
+
+ ~CMediaController() override
+ {
+ for (const auto& itRenderer : m_registeredRenderers)
+ unregisterRenderer(itRenderer);
+ m_registeredRenderers.clear();
+ }
+
+#define CHECK_USERDATA_RETURN(userdata) do { \
+ if (!g_UserData.Contains(userdata)) \
+ return; \
+ } while(0)
+
+ void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnStopResult(res, device, userdata);
+ }
+
+ void OnSetPlayModeResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetPlayModeResult(res, device, userdata);
+ }
+
+ void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetAVTransportURIResult(res, device, userdata);
+ }
+
+ void OnSeekResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSeekResult(res, device, userdata);
+ }
+
+ void OnPreviousResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPreviousResult(res, device, userdata);
+ }
+
+ void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPlayResult(res, device, userdata);
+ }
+
+ void OnPauseResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPauseResult(res, device, userdata);
+ }
+
+ void OnNextResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnNextResult(res, device, userdata);
+ }
+
+ void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetMediaInfoResult(res, device, info, userdata);
+ }
+
+ void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetPositionInfoResult(res, device, info, userdata);
+ }
+
+ void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetTransportInfoResult(res, device, info, userdata);
+ }
+
+ bool OnMRAdded(PLT_DeviceDataReference& device ) override
+ {
+ if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
+ return false;
+
+ CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ playerCoreFactory.OnPlayerDiscovered((const char*)device->GetUUID()
+ ,(const char*)device->GetFriendlyName());
+
+ m_registeredRenderers.insert(std::string(device->GetUUID().GetChars()));
+ return true;
+ }
+
+ void OnMRRemoved(PLT_DeviceDataReference& device ) override
+ {
+ if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
+ return;
+
+ std::string uuid(device->GetUUID().GetChars());
+ unregisterRenderer(uuid);
+ m_registeredRenderers.erase(uuid);
+ }
+
+private:
+ void unregisterRenderer(const std::string &deviceUUID)
+ {
+ CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ playerCoreFactory.OnPlayerRemoved(deviceUUID);
+ }
+
+ std::set<std::string> m_registeredRenderers;
+};
+
+/*----------------------------------------------------------------------
+| CUPnP::CUPnP
++---------------------------------------------------------------------*/
+CUPnP::CUPnP() :
+ m_MediaBrowser(NULL),
+ m_MediaController(NULL),
+ m_LogHandler(NULL),
+ m_ServerHolder(new CDeviceHostReferenceHolder()),
+ m_RendererHolder(new CRendererReferenceHolder()),
+ m_CtrlPointHolder(new CCtrlPointReferenceHolder())
+{
+ NPT_LogManager::GetDefault().Configure("plist:.level=FINE;.handlers=CustomHandler;");
+ NPT_LogHandler::Create("xbmc", "CustomHandler", m_LogHandler);
+ m_LogHandler->SetCustomHandlerFunction(&UPnPLogger);
+
+ // initialize upnp context
+ m_UPnP = new PLT_UPnP();
+
+ // keep main IP around
+ if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
+ m_IP = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
+ }
+ NPT_List<NPT_IpAddress> list;
+ if (NPT_SUCCEEDED(PLT_UPnPMessageHelper::GetIPAddresses(list)) && list.GetItemCount()) {
+ m_IP = (*(list.GetFirstItem())).ToString();
+ }
+ else if(m_IP.empty())
+ m_IP = "localhost";
+
+ // start upnp monitoring
+ m_UPnP->Start();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::~CUPnP
++---------------------------------------------------------------------*/
+CUPnP::~CUPnP()
+{
+ m_UPnP->Stop();
+ StopClient();
+ StopController();
+ StopServer();
+
+ delete m_UPnP;
+ delete m_LogHandler;
+ delete m_ServerHolder;
+ delete m_RendererHolder;
+ delete m_CtrlPointHolder;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::GetInstance
++---------------------------------------------------------------------*/
+CUPnP*
+CUPnP::GetInstance()
+{
+ if (!upnp) {
+ upnp = new CUPnP();
+ }
+
+ return upnp;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::ReleaseInstance
++---------------------------------------------------------------------*/
+void
+CUPnP::ReleaseInstance(bool bWait)
+{
+ if (upnp) {
+ CUPnP* _upnp = upnp;
+ upnp = NULL;
+
+ if (bWait) {
+ delete _upnp;
+ } else {
+ // since it takes a while to clean up
+ // starts a detached thread to do this
+ CUPnPCleaner* cleaner = new CUPnPCleaner(_upnp);
+ cleaner->Start();
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::GetServer
++---------------------------------------------------------------------*/
+CUPnPServer* CUPnP::GetServer()
+{
+ if(upnp)
+ return static_cast<CUPnPServer*>(upnp->m_ServerHolder->m_Device.AsPointer());
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::MarkWatched
++---------------------------------------------------------------------*/
+bool
+CUPnP::MarkWatched(const CFileItem& item, const bool watched)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->MarkWatched(item, watched);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::SaveFileState
++---------------------------------------------------------------------*/
+bool
+CUPnP::SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->SaveFileState(item, bookmark, updatePlayCount);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateControlPoint
++---------------------------------------------------------------------*/
+void
+CUPnP::CreateControlPoint()
+{
+ if (!m_CtrlPointHolder->m_CtrlPoint.IsNull())
+ return;
+
+ // create controlpoint
+ m_CtrlPointHolder->m_CtrlPoint = new PLT_CtrlPoint();
+
+ // start it
+ m_UPnP->AddCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::DestroyControlPoint
++---------------------------------------------------------------------*/
+void
+CUPnP::DestroyControlPoint()
+{
+ if (m_CtrlPointHolder->m_CtrlPoint.IsNull())
+ return;
+
+ m_UPnP->RemoveCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
+ m_CtrlPointHolder->m_CtrlPoint = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::UpdateItem
++---------------------------------------------------------------------*/
+bool
+CUPnP::UpdateItem(const std::string& path, const CFileItem& item)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->UpdateItem(path, item);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartClient
++---------------------------------------------------------------------*/
+void
+CUPnP::StartClient()
+{
+ std::unique_lock<CCriticalSection> lock(m_lockMediaBrowser);
+ if (m_MediaBrowser != NULL)
+ return;
+
+ CreateControlPoint();
+
+ // start browser
+ m_MediaBrowser = new CMediaBrowser(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopClient
++---------------------------------------------------------------------*/
+void
+CUPnP::StopClient()
+{
+ std::unique_lock<CCriticalSection> lock(m_lockMediaBrowser);
+ if (m_MediaBrowser == NULL)
+ return;
+
+ delete m_MediaBrowser;
+ m_MediaBrowser = NULL;
+
+ if (!IsControllerStarted())
+ DestroyControlPoint();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartController
++---------------------------------------------------------------------*/
+void
+CUPnP::StartController()
+{
+ if (m_MediaController != NULL)
+ return;
+
+ CreateControlPoint();
+
+ m_MediaController = new CMediaController(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopController
++---------------------------------------------------------------------*/
+void
+CUPnP::StopController()
+{
+ if (m_MediaController == NULL)
+ return;
+
+ delete m_MediaController;
+ m_MediaController = NULL;
+
+ if (!IsClientStarted())
+ DestroyControlPoint();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateServer
++---------------------------------------------------------------------*/
+CUPnPServer*
+CUPnP::CreateServer(int port /* = 0 */)
+{
+ CUPnPServer* device =
+ new CUPnPServer(CSysInfo::GetDeviceName().c_str(),
+ CUPnPSettings::GetInstance().GetServerUUID().length() ? CUPnPSettings::GetInstance().GetServerUUID().c_str() : NULL,
+ port);
+
+ // trying to set optional upnp values for XP UPnP UI Icons to detect us
+ // but it doesn't work anyways as it requires multicast for XP to detect us
+ device->m_PresentationURL =
+ NPT_HttpUrl(m_IP.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
+ "/").ToString();
+
+ device->m_ModelName = "Kodi";
+ device->m_ModelNumber = CSysInfo::GetVersion().c_str();
+ device->m_ModelDescription = "Kodi - Media Server";
+ device->m_ModelURL = "http://kodi.tv/";
+ device->m_Manufacturer = "XBMC Foundation";
+ device->m_ManufacturerURL = "http://kodi.tv/";
+
+ device->SetDelegate(device);
+ return device;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartServer
++---------------------------------------------------------------------*/
+bool
+CUPnP::StartServer()
+{
+ if (!m_ServerHolder->m_Device.IsNull()) return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // load upnpserver.xml
+ std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
+ CUPnPSettings::GetInstance().Load(filename);
+
+ // create the server with a XBox compatible friendlyname and UUID from upnpserver.xml if found
+ m_ServerHolder->m_Device = CreateServer(CUPnPSettings::GetInstance().GetServerPort());
+
+ // start server
+ NPT_Result res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
+ if (NPT_FAILED(res)) {
+ // if the upnp device port was not 0, it could have failed because
+ // of port being in used, so restart with a random port
+ if (CUPnPSettings::GetInstance().GetServerPort() > 0) m_ServerHolder->m_Device = CreateServer(0);
+
+ res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
+ }
+
+ // save port but don't overwrite saved settings if port was random
+ if (NPT_SUCCEEDED(res)) {
+ if (CUPnPSettings::GetInstance().GetServerPort() == 0) {
+ CUPnPSettings::GetInstance().SetServerPort(m_ServerHolder->m_Device->GetPort());
+ }
+ CUPnPServer::m_MaxReturnedItems = UPNP_DEFAULT_MAX_RETURNED_ITEMS;
+ if (CUPnPSettings::GetInstance().GetMaximumReturnedItems() > 0) {
+ // must be > UPNP_DEFAULT_MIN_RETURNED_ITEMS
+ CUPnPServer::m_MaxReturnedItems = std::max(UPNP_DEFAULT_MIN_RETURNED_ITEMS, CUPnPSettings::GetInstance().GetMaximumReturnedItems());
+ }
+ CUPnPSettings::GetInstance().SetMaximumReturnedItems(CUPnPServer::m_MaxReturnedItems);
+ }
+
+ // save UUID
+ CUPnPSettings::GetInstance().SetServerUUID(m_ServerHolder->m_Device->GetUUID().GetChars());
+ return CUPnPSettings::GetInstance().Save(filename);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopServer
++---------------------------------------------------------------------*/
+void
+CUPnP::StopServer()
+{
+ if (m_ServerHolder->m_Device.IsNull()) return;
+
+ m_UPnP->RemoveDevice(m_ServerHolder->m_Device);
+ m_ServerHolder->m_Device = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer*
+CUPnP::CreateRenderer(int port /* = 0 */)
+{
+ CUPnPRenderer* device =
+ new CUPnPRenderer(CSysInfo::GetDeviceName().c_str(),
+ false,
+ (CUPnPSettings::GetInstance().GetRendererUUID().length() ? CUPnPSettings::GetInstance().GetRendererUUID().c_str() : NULL),
+ port);
+
+ device->m_PresentationURL =
+ NPT_HttpUrl(m_IP.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
+ "/").ToString();
+ device->m_ModelName = "Kodi";
+ device->m_ModelNumber = CSysInfo::GetVersion().c_str();
+ device->m_ModelDescription = "Kodi - Media Renderer";
+ device->m_ModelURL = "http://kodi.tv/";
+ device->m_Manufacturer = "XBMC Foundation";
+ device->m_ManufacturerURL = "http://kodi.tv/";
+
+ return device;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartRenderer
++---------------------------------------------------------------------*/
+bool CUPnP::StartRenderer()
+{
+ if (!m_RendererHolder->m_Device.IsNull())
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
+ CUPnPSettings::GetInstance().Load(filename);
+
+ m_RendererHolder->m_Device = CreateRenderer(CUPnPSettings::GetInstance().GetRendererPort());
+
+ NPT_Result res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
+
+ // failed most likely because port is in use, try again with random port now
+ if (NPT_FAILED(res) && CUPnPSettings::GetInstance().GetRendererPort() != 0) {
+ m_RendererHolder->m_Device = CreateRenderer(0);
+
+ res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
+ }
+
+ // save port but don't overwrite saved settings if random
+ if (NPT_SUCCEEDED(res) && CUPnPSettings::GetInstance().GetRendererPort() == 0) {
+ CUPnPSettings::GetInstance().SetRendererPort(m_RendererHolder->m_Device->GetPort());
+ }
+
+ // save UUID
+ CUPnPSettings::GetInstance().SetRendererUUID(m_RendererHolder->m_Device->GetUUID().GetChars());
+ return CUPnPSettings::GetInstance().Save(filename);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopRenderer
++---------------------------------------------------------------------*/
+void CUPnP::StopRenderer()
+{
+ if (m_RendererHolder->m_Device.IsNull()) return;
+
+ m_UPnP->RemoveDevice(m_RendererHolder->m_Device);
+ m_RendererHolder->m_Device = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::UpdateState
++---------------------------------------------------------------------*/
+void CUPnP::UpdateState()
+{
+ if (!m_RendererHolder->m_Device.IsNull())
+ static_cast<CUPnPRenderer*>(m_RendererHolder->m_Device.AsPointer())->UpdateState();
+}
+
+void CUPnP::RegisterUserdata(void* ptr)
+{
+ NPT_AutoLock lock(g_UserDataLock);
+ g_UserData.Add(ptr);
+}
+
+void CUPnP::UnregisterUserdata(void* ptr)
+{
+ NPT_AutoLock lock(g_UserDataLock);
+ g_UserData.Remove(ptr);
+}
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnP.h b/xbmc/network/upnp/UPnP.h
new file mode 100644
index 0000000..311aa6c
--- /dev/null
+++ b/xbmc/network/upnp/UPnP.h
@@ -0,0 +1,108 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <string>
+
+class NPT_LogHandler;
+class PLT_UPnP;
+class PLT_SyncMediaBrowser;
+class PLT_MediaController;
+class PLT_MediaObject;
+class PLT_MediaItemResource;
+class CFileItem;
+class CBookmark;
+
+namespace UPNP
+{
+
+class CDeviceHostReferenceHolder;
+class CCtrlPointReferenceHolder;
+class CRendererReferenceHolder;
+class CUPnPRenderer;
+class CUPnPServer;
+
+class CUPnP
+{
+public:
+ CUPnP();
+ ~CUPnP();
+
+ // server
+ bool StartServer();
+ void StopServer();
+
+ // client
+ void StartClient();
+ void StopClient();
+ bool IsClientStarted() { return (m_MediaBrowser != NULL); }
+
+ // controller
+ void StartController();
+ void StopController();
+ bool IsControllerStarted() { return (m_MediaController != NULL); }
+
+ // renderer
+ bool StartRenderer();
+ void StopRenderer();
+ void UpdateState();
+
+ // class methods
+ static CUPnP* GetInstance();
+ static CUPnPServer* GetServer();
+ static void ReleaseInstance(bool bWait);
+ static bool IsInstantiated() { return upnp != NULL; }
+
+ static bool MarkWatched(const CFileItem& item,
+ const bool watched);
+
+ static bool SaveFileState(const CFileItem& item,
+ const CBookmark& bookmark,
+ const bool updatePlayCount);
+ static bool UpdateItem(const std::string& path,
+ const CFileItem& item);
+
+ static void RegisterUserdata(void* ptr);
+ static void UnregisterUserdata(void* ptr);
+private:
+ CUPnP(const CUPnP&) = delete;
+ CUPnP& operator=(const CUPnP&) = delete;
+
+ void CreateControlPoint();
+ void DestroyControlPoint();
+
+ // methods
+ CUPnPRenderer* CreateRenderer(int port = 0);
+ CUPnPServer* CreateServer(int port = 0);
+
+ CCriticalSection m_lockMediaBrowser;
+
+ public:
+ PLT_SyncMediaBrowser* m_MediaBrowser;
+ PLT_MediaController* m_MediaController;
+
+private:
+ std::string m_IP;
+ PLT_UPnP* m_UPnP;
+ NPT_LogHandler* m_LogHandler;
+ CDeviceHostReferenceHolder* m_ServerHolder;
+ CRendererReferenceHolder* m_RendererHolder;
+ CCtrlPointReferenceHolder* m_CtrlPointHolder;
+
+
+ static CUPnP* upnp;
+};
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPInternal.cpp b/xbmc/network/upnp/UPnPInternal.cpp
new file mode 100644
index 0000000..4066157
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.cpp
@@ -0,0 +1,1245 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPInternal.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnPServer.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ContentUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+#include <array>
+#include <string_view>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+namespace UPNP
+{
+
+// the original version of content type here,eg: text/srt,which was defined 10 years ago (year 2013,commit 56519bec #L1158-L1161 )
+// is not a standard mime type. according to the specs of UPNP
+// http://upnp.org/specs/av/UPnP-av-ConnectionManager-v3-Service.pdf chapter "A.1.1 ProtocolInfo Definition"
+// "The <contentFormat> part for HTTP GET is described by a MIME type RFC https://www.ietf.org/rfc/rfc1341.txt"
+// all the pre-defined "text/*" MIME by IANA is here https://www.iana.org/assignments/media-types/media-types.xhtml#text
+// there is not any subtitle MIME for now (year 2022), we used to use text/srt|ssa|sub|idx, but,
+// kodi support SUP subtitle now, and SUP subtitle is not really a text(see below), to keep it
+// compatible, we suggest only to match the extension
+//
+// main purpose of this array is to share supported real subtitle formats when kodi act as a UPNP
+// server or UPNP/DLNA media render
+constexpr std::array<std::string_view, 9> SupportedSubFormats = {
+ "txt", "srt", "ssa", "ass", "sub", "smi", "vtt",
+ // "sup" subtitle is not a real TEXT,
+ // and there is no real STD subtitle RFC of DLNA,
+ // so we only match the extension of the "fake" content type
+ "sup", "idx"};
+
+// Map defining extensions for mimetypes not available in Platinum mimetype map
+// or that the application wants to override. These definitions take precedence
+// over all other possible mime type definitions.
+constexpr NPT_HttpFileRequestHandler_DefaultFileTypeMapEntry kodiPlatinumMimeTypeExtensions[] = {
+ {"m2ts", "video/vnd.dlna.mpeg-tts"}};
+
+/*----------------------------------------------------------------------
+| GetClientQuirks
++---------------------------------------------------------------------*/
+EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context)
+{
+ if(context == NULL)
+ return ECLIENTQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+ const NPT_String* user_agent = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_USER_AGENT);
+ const NPT_String* server = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_SERVER);
+
+ if (user_agent) {
+ if (user_agent->Find("XBox", 0, true) >= 0 ||
+ user_agent->Find("Xenon", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+
+ if (user_agent->Find("Windows-Media-Player", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_UNKNOWNSERIES;
+
+ }
+ if (server) {
+ if (server->Find("Xbox", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+ }
+
+ return (EClientQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMediaControllerQuirks
++---------------------------------------------------------------------*/
+EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device)
+{
+ if (device == NULL)
+ return EMEDIACONTROLLERQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+
+ if (device->m_Manufacturer.Find("Samsung Electronics") >= 0)
+ quirks |= EMEDIACONTROLLERQUIRKS_X_MKV;
+
+ return (EMediaControllerQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const char* filename,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String ext = URIUtils::GetExtension(filename).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ return PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const CFileItem& item,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ std::string path = item.GetPath();
+ if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->GetPath().empty()) {
+ path = item.GetVideoInfoTag()->GetPath();
+ } else if (item.HasMusicInfoTag() && !item.GetMusicInfoTag()->GetURL().empty()) {
+ path = item.GetMusicInfoTag()->GetURL();
+ }
+
+ if (URIUtils::IsStack(path))
+ path = XFILE::CStackDirectory::GetFirstStackedFile(path);
+
+ NPT_String ext = URIUtils::GetExtension(path).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ NPT_String mime;
+
+ if (!ext.IsEmpty())
+ {
+ /* We look first to our extensions/overrides of libplatinum mimetypes. If not found, fallback to
+ Platinum definitions.
+ */
+ const auto kodiOverrideMimeType = std::find_if(
+ std::begin(kodiPlatinumMimeTypeExtensions), std::end(kodiPlatinumMimeTypeExtensions),
+ [&](const auto& mimeTypeEntry) { return mimeTypeEntry.extension == ext; });
+ if (kodiOverrideMimeType != std::end(kodiPlatinumMimeTypeExtensions))
+ {
+ mime = kodiOverrideMimeType->mime_type;
+ }
+ else
+ {
+ /* Give priority to Platinum mime types as they are defined to map extension to DLNA compliant mime types
+ or custom types according to context (who asked for it)
+ */
+ mime = PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+ if (mime == "application/octet-stream")
+ {
+ mime = "";
+ }
+ }
+ }
+
+ /* if Platinum couldn't map it, default to Kodi internal mapping */
+ if (mime.IsEmpty()) {
+ NPT_String mime = item.GetMimeType().c_str();
+ if (mime == "application/octet-stream") mime = "";
+ }
+
+ /* fallback to generic mime type if not found */
+ if (mime.IsEmpty()) {
+ if (item.IsVideo() || item.IsVideoDb() )
+ mime = "video/" + ext;
+ else if (item.IsAudio() || item.IsMusicDb() )
+ mime = "audio/" + ext;
+ else if (item.IsPicture() )
+ mime = "image/" + ext;
+ else if (item.IsSubtitle())
+ mime = "text/" + ext;
+ }
+
+ /* nothing we can figure out */
+ if (mime.IsEmpty()) {
+ mime = "application/octet-stream";
+ }
+
+ return mime;
+}
+
+/*----------------------------------------------------------------------
+| GetProtocolInfo
++---------------------------------------------------------------------*/
+const NPT_String
+GetProtocolInfo(const CFileItem& item,
+ const char* protocol,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String proto = protocol;
+
+ //! @todo fixup the protocol just in case nothing was passed
+ if (proto.IsEmpty()) {
+ proto = item.GetURL().GetProtocol().c_str();
+ }
+
+ /**
+ * map protocol to right prefix and use xbmc-get for
+ * unsupported UPnP protocols for other xbmc clients
+ * @todo add rtsp ?
+ */
+ if (proto == "http") {
+ proto = "http-get";
+ } else {
+ proto = "xbmc-get";
+ }
+
+ /* we need a valid extension to retrieve the mimetype for the protocol info */
+ NPT_String mime = GetMimeType(item, context);
+ proto += ":*:" + mime + ":" + PLT_ProtocolInfo::GetDlnaExtension(mime, context);
+ return proto;
+}
+
+ /*----------------------------------------------------------------------
+ | CResourceFinder
+ +---------------------------------------------------------------------*/
+CResourceFinder::CResourceFinder(const char* protocol, const char* content)
+ : m_Protocol(protocol)
+ , m_Content(content)
+{
+}
+
+bool CResourceFinder::operator()(const PLT_MediaItemResource& resource) const {
+ if (m_Content.IsEmpty())
+ return (resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0);
+ else
+ return ((resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0)
+ && resource.m_ProtocolInfo.GetContentType().StartsWith(m_Content, true));
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.GetURL().empty() && file_path)
+ *file_path = tag.GetURL().c_str();
+
+ std::vector<std::string> genres = tag.GetGenre();
+ for (unsigned int index = 0; index < genres.size(); index++)
+ object.m_Affiliation.genres.Add(genres.at(index).c_str());
+ object.m_Title = tag.GetTitle().c_str();
+ object.m_Affiliation.album = tag.GetAlbum().c_str();
+ for (unsigned int index = 0; index < tag.GetArtist().size(); index++)
+ {
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str());
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str(), "Performer");
+ }
+ object.m_People.artists.Add((!tag.GetAlbumArtistString().empty() ? tag.GetAlbumArtistString() : tag.GetArtistString()).c_str(), "AlbumArtist");
+ if(tag.GetAlbumArtistString().empty())
+ object.m_Creator = tag.GetArtistString().c_str();
+ else
+ object.m_Creator = tag.GetAlbumArtistString().c_str();
+ object.m_MiscInfo.original_track_number = tag.GetTrackNumber();
+ if(tag.GetDatabaseId() >= 0) {
+ object.m_ReferenceID = NPT_String::Format("musicdb://songs/%i%s", tag.GetDatabaseId(), URIUtils::GetExtension(tag.GetURL()).c_str());
+ }
+ if (object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ object.m_MiscInfo.last_time = tag.GetLastPlayed().GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+
+ if (resource) resource->m_Duration = tag.GetDuration();
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.m_strFileNameAndPath.empty() && file_path)
+ *file_path = tag.m_strFileNameAndPath.c_str();
+
+ if (tag.m_iDbId != -1 ) {
+ if (tag.m_type == MediaTypeMusicVideo) {
+ object.m_ObjectClass.type = "object.item.videoItem.musicVideoClip";
+ object.m_Creator = StringUtils::Join(tag.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator).c_str();
+ for (const auto& itArtist : tag.m_artist)
+ object.m_People.artists.Add(itArtist.c_str());
+ object.m_Affiliation.album = tag.m_strAlbum.c_str();
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://musicvideos/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeMovie) {
+ object.m_ObjectClass.type = "object.item.videoItem.movie";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://movies/titles/%i", tag.m_iDbId);
+ } else {
+ object.m_Recorded.series_title = tag.m_strShowTitle.c_str();
+
+ if (tag.m_type == MediaTypeTvShow) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastShow";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeSeason) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastSeason";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i", tag.m_iIdShow, tag.m_iSeason);
+ } else {
+ object.m_ObjectClass.type = "object.item.videoItem.videoBroadcast";
+ object.m_Recorded.program_title = "S" + ("0" + NPT_String::FromInteger(tag.m_iSeason)).Right(2);
+ object.m_Recorded.program_title += "E" + ("0" + NPT_String::FromInteger(tag.m_iEpisode)).Right(2);
+ object.m_Recorded.program_title += (" : " + tag.m_strTitle).c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Title = object.m_Recorded.series_title + " - " + object.m_Recorded.program_title;
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i/%i", tag.m_iIdShow, tag.m_iSeason, tag.m_iDbId);
+ object.m_Date = tag.m_firstAired.GetAsW3CDate().c_str();
+ }
+ }
+ }
+
+ if(quirks & ECLIENTQUIRKS_BASICVIDEOCLASS)
+ object.m_ObjectClass.type = "object.item.videoItem";
+
+ if(object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ for (unsigned int index = 0; index < tag.m_studio.size(); index++)
+ object.m_People.publisher.Add(tag.m_studio[index].c_str());
+
+ object.m_XbmcInfo.date_added = tag.m_dateAdded.GetAsW3CDate().c_str();
+ object.m_XbmcInfo.rating = tag.GetRating().rating;
+ object.m_XbmcInfo.votes = tag.GetRating().votes;
+ object.m_XbmcInfo.unique_identifier = tag.GetUniqueID().c_str();
+ for (const auto& country : tag.m_country)
+ object.m_XbmcInfo.countries.Add(country.c_str());
+ object.m_XbmcInfo.user_rating = tag.m_iUserRating;
+
+ for (unsigned int index = 0; index < tag.m_genre.size(); index++)
+ object.m_Affiliation.genres.Add(tag.m_genre.at(index).c_str());
+
+ for (CVideoInfoTag::iCast it = tag.m_cast.begin(); it != tag.m_cast.end(); ++it)
+ {
+ object.m_People.actors.Add(it->strName.c_str(), it->strRole.c_str());
+ }
+
+ for (unsigned int index = 0; index < tag.m_director.size(); index++)
+ object.m_People.directors.Add(tag.m_director[index].c_str());
+
+ for (unsigned int index = 0; index < tag.m_writingCredits.size(); index++)
+ object.m_People.authors.Add(tag.m_writingCredits[index].c_str());
+
+ object.m_Description.description = tag.m_strTagLine.c_str();
+ object.m_Description.long_description = tag.m_strPlot.c_str();
+ object.m_Description.rating = tag.m_strMPAARating.c_str();
+ object.m_MiscInfo.last_position = (NPT_UInt32)tag.GetResumePoint().timeInSeconds;
+ object.m_XbmcInfo.last_playerstate = tag.GetResumePoint().playerState.c_str();
+ object.m_MiscInfo.last_time = tag.m_lastPlayed.GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+ if (resource) {
+ resource->m_Duration = tag.GetDuration();
+ if (tag.HasStreamDetails()) {
+ const CStreamDetails &details = tag.m_streamDetails;
+ resource->m_Resolution = NPT_String::FromInteger(details.GetVideoWidth()) + "x" + NPT_String::FromInteger(details.GetVideoHeight());
+ resource->m_NbAudioChannels = details.GetAudioChannels();
+ }
+ }
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| BuildObject
++---------------------------------------------------------------------*/
+PLT_MediaObject*
+BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context /* = NULL */,
+ CUPnPServer* upnp_server /* = NULL */,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("UPNP::BuildObject");
+
+ PLT_MediaItemResource resource;
+ PLT_MediaObject* object = NULL;
+ std::string thumb;
+
+ logger->debug("Building didl for object '{}'", item.GetPath());
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return nullptr;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return nullptr;
+
+ EClientQuirks quirks = GetClientQuirks(context);
+
+ // get list of ip addresses
+ NPT_List<NPT_IpAddress> ips;
+ NPT_HttpUrl rooturi;
+ NPT_CHECK_LABEL(PLT_UPnPMessageHelper::GetIPAddresses(ips), failure);
+
+ // if we're passed an interface where we received the request from
+ // move the ip to the top
+ if (context && context->GetLocalAddress().GetIpAddress().ToString() != "0.0.0.0")
+ {
+ rooturi = NPT_HttpUrl(context->GetLocalAddress().GetIpAddress().ToString(),
+ context->GetLocalAddress().GetPort(), "/");
+ ips.Remove(context->GetLocalAddress().GetIpAddress());
+ ips.Insert(ips.GetFirstItem(), context->GetLocalAddress().GetIpAddress());
+ } else if(upnp_server) {
+ rooturi = NPT_HttpUrl("localhost", upnp_server->GetPort(), "/");
+ }
+
+ if (!item.m_bIsFolder) {
+ object = new PLT_MediaItem();
+ object->m_ObjectID = item.GetPath().c_str();
+
+ /* Setup object type */
+ if (item.IsMusicDb() || item.IsAudio()) {
+ object->m_ObjectClass.type = "object.item.audioItem.musicTrack";
+
+ if (item.HasMusicInfoTag()) {
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsVideoDb() || item.IsVideo()) {
+ object->m_ObjectClass.type = "object.item.videoItem";
+
+ if(quirks & ECLIENTQUIRKS_UNKNOWNSERIES)
+ object->m_Affiliation.album = "[Unknown Series]";
+
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsPicture()) {
+ object->m_ObjectClass.type = "object.item.imageItem.photo";
+ } else {
+ object->m_ObjectClass.type = "object.item";
+ }
+
+ // duration of zero is invalid
+ if (resource.m_Duration == 0) resource.m_Duration = -1;
+
+ // Set the resource file size
+ resource.m_Size = item.m_dwSize;
+ if(resource.m_Size == 0)
+ resource.m_Size = (NPT_LargeSize)-1;
+
+ // set date
+ if (object->m_Date.IsEmpty() && item.m_dateTime.IsValid()) {
+ object->m_Date = item.m_dateTime.GetAsW3CDate().c_str();
+ }
+
+ if (upnp_server) {
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, file_path, GetProtocolInfo(item, "http", context));
+ }
+
+ // if the item is remote, add a direct link to the item
+ if (URIUtils::IsRemote((const char*)file_path)) {
+ resource.m_ProtocolInfo = PLT_ProtocolInfo(GetProtocolInfo(item, item.GetURL().GetProtocol().c_str(), context));
+ resource.m_Uri = file_path;
+
+ // if the direct link can be served directly using http, then push it in front
+ // otherwise keep the xbmc-get resource last and let a compatible client look for it
+ if (resource.m_ProtocolInfo.ToString().StartsWith("xbmc", true)) {
+ object->m_Resources.Add(resource);
+ } else {
+ object->m_Resources.Insert(object->m_Resources.GetFirstItem(), resource);
+ }
+ }
+
+ // copy across the known metadata
+ for(unsigned i=0; i<object->m_Resources.GetItemCount(); i++) {
+ object->m_Resources[i].m_Size = resource.m_Size;
+ object->m_Resources[i].m_Duration = resource.m_Duration;
+ object->m_Resources[i].m_Resolution = resource.m_Resolution;
+ }
+
+ // Some upnp clients expect all audio items to have parent root id 4
+#ifdef WMP_ID_MAPPING
+ object->m_ParentID = "4";
+#endif
+ } else {
+ PLT_MediaContainer* container = new PLT_MediaContainer;
+ object = container;
+
+ /* Assign a title and id for this container */
+ container->m_ObjectID = item.GetPath().c_str();
+ container->m_ObjectClass.type = "object.container";
+ container->m_ChildrenCount = -1;
+
+ /* this might be overkill, but hey */
+ if (item.IsMusicDb()) {
+ MUSICDATABASEDIRECTORY::NODE_TYPE node = CMusicDatabaseDirectory::GetDirectoryType(item.GetPath());
+ switch(node) {
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ARTIST: {
+ container->m_ObjectClass.type += ".person.musicArtist";
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack((!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString())).c_str(), "AlbumArtist");
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all artists to have parent root id 107
+ container->m_ParentID = "107";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM:
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED: {
+ container->m_ObjectClass.type += ".album.musicAlbum";
+ // for Sonos to be happy
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString()).c_str(), "AlbumArtist");
+ container->m_Affiliation.album = CorrectAllItemsSortHack(tag->GetAlbum()).c_str();
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all albums to have parent root id 7
+ container->m_ParentID = "7";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.musicGenre";
+ break;
+ default:
+ break;
+ }
+ } else if (item.IsVideoDb()) {
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = CVideoDatabaseDirectory::GetDirectoryType(item.GetPath());
+ CVideoInfoTag &tag = *item.GetVideoInfoTag();
+ switch(node) {
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.movieGenre";
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR:
+ container->m_ObjectClass.type += ".person.videoArtist";
+ container->m_Creator =
+ StringUtils::Join(
+ tag.m_artist,
+ settingsComponent->GetAdvancedSettings()->m_videoItemSeparator)
+ .c_str();
+ container->m_Title = tag.m_strTitle.c_str();
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastSeason";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastShow";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ default:
+ container->m_ObjectClass.type += ".storageFolder";
+ break;
+ }
+ } else if (item.IsPlayList() || item.IsSmartPlayList()) {
+ container->m_ObjectClass.type += ".playlistContainer";
+ }
+
+ if(quirks & ECLIENTQUIRKS_ONLYSTORAGEFOLDER) {
+ container->m_ObjectClass.type = "object.container.storageFolder";
+ }
+
+ /* Get the number of children for this container */
+ if (with_count && upnp_server) {
+ if (object->m_ObjectID.StartsWith("virtualpath://")) {
+ NPT_LargeSize count = 0;
+ NPT_CHECK_LABEL(NPT_File::GetSize(file_path, count), failure);
+ container->m_ChildrenCount = (NPT_Int32)count;
+ } else {
+ /* this should be a standard path */
+ //! @todo - get file count of this directory
+ }
+ }
+ }
+
+ // set a title for the object
+ if (object->m_Title.IsEmpty()) {
+ if (!item.GetLabel().empty()) {
+ std::string title = item.GetLabel();
+ if (item.IsPlayList() || !item.m_bIsFolder) URIUtils::RemoveExtension(title);
+ object->m_Title = title.c_str();
+ }
+ }
+
+ if (upnp_server) {
+ // determine the correct artwork for this item
+ if (!thumb_loader.IsNull())
+ thumb_loader->LoadItem(&item);
+
+ // we have to decide the best art type to serve to the client - use ContentUtils
+ // to get it since it depends on the mediatype of the item being served
+ thumb = ContentUtils::GetPreferredArtImage(item);
+
+ if (!thumb.empty()) {
+ PLT_AlbumArtInfo art;
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(thumb, ".png"))
+ {
+ art.dlna_profile = "PNG_TN";
+ }
+ else
+ {
+ art.dlna_profile = "JPEG_TN";
+ }
+ // append /thumb to the safe resource uri to avoid clients flagging the item with
+ // the incorrect mimetype (derived from the file extension)
+ art.uri = upnp_server->BuildSafeResourceUri(
+ rooturi, (*ips.GetFirstItem()).ToString(),
+ std::string(CTextureUtils::GetWrappedImageURL(thumb) + "/thumb").c_str());
+ object->m_ExtraInfo.album_arts.Add(art);
+ }
+
+ for (const auto& itArtwork : item.GetArt())
+ {
+ if (!itArtwork.first.empty() && !itArtwork.second.empty())
+ {
+ std::string wrappedUrl = CTextureUtils::GetWrappedImageURL(itArtwork.second);
+ object->m_XbmcInfo.artwork.Add(
+ itArtwork.first.c_str(),
+ upnp_server->BuildSafeResourceUri(rooturi, (*ips.GetFirstItem()).ToString(),
+ wrappedUrl.c_str()));
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, wrappedUrl.c_str(),
+ ("xbmc.org:*:" + itArtwork.first + ":*").c_str());
+ }
+ }
+ }
+
+ // look for and add external subtitle if we are processing a video file and
+ // we are being called by a UPnP player or renderer or the user has chosen
+ // to look for external subtitles
+ if (upnp_server != NULL && item.IsVideo() &&
+ (upnp_service == UPnPPlayer || upnp_service == UPnPRenderer ||
+ settings->GetBool(CSettings::SETTING_SERVICES_UPNPLOOKFOREXTERNALSUBTITLES)))
+ {
+ // find any available external subtitles
+ std::vector<std::string> filenames;
+ std::vector<std::string> subtitles;
+ CUtil::ScanForExternalSubtitles(file_path.GetChars(), filenames);
+
+ std::string ext;
+ for (unsigned int i = 0; i < filenames.size(); i++)
+ {
+ ext = URIUtils::GetExtension(filenames[i]).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+ /* Hardcoded check for extension is not the best way, but it can't be allowed to pass all
+ subtitle extension (ex. rar or zip). There are the most popular extensions support by UPnP devices.*/
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == ext)
+ {
+ subtitles.push_back(filenames[i]);
+ }
+ }
+ }
+
+ std::string subtitlePath;
+
+ if (subtitles.size() == 1)
+ {
+ subtitlePath = subtitles[0];
+ }
+ else if (!subtitles.empty())
+ {
+ std::string preferredLanguage{"en"};
+
+ /* trying to find subtitle with preferred language settings */
+ auto setting = settings->GetSetting("locale.subtitlelanguage");
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", "locale.subtitlelanguage");
+ else
+ preferredLanguage = setting->ToString();
+
+ std::string preferredLanguageCode;
+ g_LangCodeExpander.ConvertToISO6392B(preferredLanguage, preferredLanguageCode);
+
+ for (unsigned int i = 0; i < subtitles.size(); i++)
+ {
+ ExternalStreamInfo info =
+ CUtil::GetExternalStreamDetailsFromFilename(file_path.GetChars(), subtitles[i]);
+
+ if (preferredLanguageCode == info.language)
+ {
+ subtitlePath = subtitles[i];
+ break;
+ }
+ }
+ /* if not found subtitle with preferred language, get the first one */
+ if (subtitlePath.empty())
+ {
+ subtitlePath = subtitles[0];
+ }
+ }
+
+ if (!subtitlePath.empty())
+ {
+ /* subtitles are added as 2 resources, 2 sec resources and 1 addon to video resource, to be compatible with
+ the most of the devices; all UPnP devices take the last one it could handle,
+ and skip ones it doesn't "understand" */
+ // add subtitle resource with standard protocolInfo
+ NPT_String protocolInfo = GetProtocolInfo(CFileItem(subtitlePath, false), "http", context);
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+ // add subtitle resource with smi/caption protocol info (some devices)
+ PLT_ProtocolInfo protInfo = PLT_ProtocolInfo(protocolInfo);
+ protocolInfo = protInfo.GetProtocol() + ":" + protInfo.GetMask() + ":smi/caption:" + protInfo.GetExtra();
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+
+ ext = URIUtils::GetExtension(subtitlePath).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+ NPT_String subtitle_uri = object->m_Resources[object->m_Resources.GetItemCount() - 1].m_Uri;
+
+ // add subtitle to video resource (the first one) (for some devices)
+ object->m_Resources[0].m_CustomData["xmlns:pv"] = "http://www.pv.com/pvns/";
+ object->m_Resources[0].m_CustomData["pv:subtitleFileUri"] = subtitle_uri;
+ object->m_Resources[0].m_CustomData["pv:subtitleFileType"] = ext.c_str();
+
+ // for samsung devices
+ PLT_SecResource sec_res;
+ sec_res.name = "CaptionInfoEx";
+ sec_res.value = subtitle_uri;
+ sec_res.attributes["type"] = ext.c_str();
+ object->m_SecResources.Add(sec_res);
+ sec_res.name = "CaptionInfo";
+ object->m_SecResources.Add(sec_res);
+
+ // adding subtitle uri for movie md5, for later use in http response
+ NPT_String movie_md5 = object->m_Resources[0].m_Uri;
+ movie_md5 = movie_md5.Right(movie_md5.GetLength() - movie_md5.Find("/%25/") - 5);
+ upnp_server->AddSubtitleUriForSecResponse(movie_md5, subtitle_uri);
+ }
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CorrectAllItemsSortHack
++---------------------------------------------------------------------*/
+const std::string&
+CorrectAllItemsSortHack(const std::string &item)
+{
+ // This is required as in order for the "* All Albums" etc. items to sort
+ // correctly, they must have fake artist/album etc. information generated.
+ // This looks nasty if we attempt to render it to the GUI, thus this (further)
+ // workaround
+ if ((item.size() == 1 && item[0] == 0x01) || (item.size() > 1 && ((unsigned char) item[1]) == 0xff))
+ return StringUtils::Empty;
+
+ return item;
+}
+
+int
+PopulateTagFromObject(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ tag.SetTitle((const char*)object.m_Title);
+ tag.SetArtist((const char*)object.m_Creator);
+ for(PLT_PersonRoles::Iterator it = object.m_People.artists.GetFirstItem(); it; it++) {
+ if (it->role == "") tag.SetArtist((const char*)it->name);
+ else if(it->role == "Performer") tag.SetArtist((const char*)it->name);
+ else if(it->role == "AlbumArtist") tag.SetAlbumArtist((const char*)it->name);
+ }
+ tag.SetTrackNumber(object.m_MiscInfo.original_track_number);
+
+ for (NPT_List<NPT_String>::Iterator it = object.m_Affiliation.genres.GetFirstItem(); it; it++) {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (it == object.m_Affiliation.genres.GetFirstItem() && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *it == "Unknown")
+ break;
+
+ tag.SetGenre((const char*) *it);
+ }
+
+ tag.SetAlbum((const char*)object.m_Affiliation.album);
+ CDateTime last;
+ last.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetLastPlayed(last);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+ if(resource)
+ tag.SetDuration(resource->m_Duration);
+ tag.SetLoaded();
+ return NPT_SUCCESS;
+}
+
+int
+PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ CDateTime date;
+ date.SetFromW3CDate((const char*)object.m_Date);
+
+ if(!object.m_Recorded.program_title.IsEmpty() || object.m_ObjectClass.type == "object.item.videoItem.videoBroadcast")
+ {
+ tag.m_type = MediaTypeEpisode;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ if (date.IsValid())
+ tag.m_firstAired = date;
+
+ int title = object.m_Recorded.program_title.Find(" : ");
+ if (title >= 0)
+ tag.m_strTitle = object.m_Recorded.program_title.SubString(title + 3);
+ else
+ tag.m_strTitle = object.m_Recorded.program_title;
+
+ int episode;
+ int season;
+ if (object.m_Recorded.episode_number > 0 && object.m_Recorded.episode_season < (NPT_UInt32)-1) {
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ } else if(sscanf(object.m_Recorded.program_title, "S%2dE%2d", &season, &episode) == 2 && title >= 0) {
+ tag.m_iEpisode = episode;
+ tag.m_iSeason = season;
+ } else {
+ tag.m_iSeason = object.m_Recorded.episode_number / 100;
+ tag.m_iEpisode = object.m_Recorded.episode_number % 100;
+ }
+ }
+ else {
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.m_premiered = date;
+
+ if (!object.m_Recorded.series_title.IsEmpty()) {
+ if (object.m_ObjectClass.type == "object.container.album.videoAlbum.videoBroadcastSeason") {
+ tag.m_type = MediaTypeSeason;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ }
+ else {
+ tag.m_type = MediaTypeTvShow;
+ tag.m_strShowTitle = object.m_Title;
+ }
+
+ if (object.m_Recorded.episode_count > 0)
+ tag.m_iEpisode = object.m_Recorded.episode_count;
+ else
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ }
+ else if(object.m_ObjectClass.type == "object.item.videoItem.musicVideoClip") {
+ tag.m_type = MediaTypeMusicVideo;
+
+ if (object.m_People.artists.GetItemCount() > 0) {
+ for (unsigned int index = 0; index < object.m_People.artists.GetItemCount(); index++)
+ tag.m_artist.emplace_back(object.m_People.artists.GetItem(index)->name.GetChars());
+ }
+ else if (!object.m_Creator.IsEmpty() && object.m_Creator != "Unknown")
+ tag.m_artist = StringUtils::Split(object.m_Creator.GetChars(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ tag.m_strAlbum = object.m_Affiliation.album;
+ }
+ else
+ tag.m_type = MediaTypeMovie;
+
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.SetPremiered(date);
+ }
+
+ for (unsigned int index = 0; index < object.m_People.publisher.GetItemCount(); index++)
+ tag.m_studio.emplace_back(object.m_People.publisher.GetItem(index)->GetChars());
+
+ tag.m_dateAdded.SetFromW3CDate((const char*)object.m_XbmcInfo.date_added);
+ tag.SetRating(object.m_XbmcInfo.rating, object.m_XbmcInfo.votes);
+ tag.SetUniqueID(object.m_XbmcInfo.unique_identifier.GetChars());
+ for (unsigned int index = 0; index < object.m_XbmcInfo.countries.GetItemCount(); index++)
+ tag.m_country.emplace_back(object.m_XbmcInfo.countries.GetItem(index)->GetChars());
+ tag.m_iUserRating = object.m_XbmcInfo.user_rating;
+
+ for (unsigned int index = 0; index < object.m_Affiliation.genres.GetItemCount(); index++)
+ {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (index == 0 && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *object.m_Affiliation.genres.GetItem(index) == "Unknown")
+ break;
+
+ tag.m_genre.emplace_back(object.m_Affiliation.genres.GetItem(index)->GetChars());
+ }
+ for (unsigned int index = 0; index < object.m_People.directors.GetItemCount(); index++)
+ tag.m_director.emplace_back(object.m_People.directors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.authors.GetItemCount(); index++)
+ tag.m_writingCredits.emplace_back(object.m_People.authors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.actors.GetItemCount(); index++)
+ {
+ SActorInfo info;
+ info.strName = object.m_People.actors.GetItem(index)->name;
+ info.strRole = object.m_People.actors.GetItem(index)->role;
+ tag.m_cast.push_back(info);
+ }
+ tag.m_strTagLine = object.m_Description.description;
+ tag.m_strPlot = object.m_Description.long_description;
+ tag.m_strMPAARating = object.m_Description.rating;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ tag.m_lastPlayed.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+
+ if(resource)
+ {
+ if (resource->m_Duration)
+ tag.SetDuration(resource->m_Duration);
+ if (object.m_MiscInfo.last_position > 0 )
+ {
+ tag.SetResumePoint(object.m_MiscInfo.last_position,
+ resource->m_Duration,
+ object.m_XbmcInfo.last_playerstate.GetChars());
+ }
+ if (!resource->m_Resolution.IsEmpty())
+ {
+ int width, height;
+ if (sscanf(resource->m_Resolution, "%dx%d", &width, &height) == 2)
+ {
+ CStreamDetailVideo* detail = new CStreamDetailVideo;
+ detail->m_iWidth = width;
+ detail->m_iHeight = height;
+ detail->m_iDuration = tag.GetDuration();
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ if (resource->m_NbAudioChannels > 0)
+ {
+ CStreamDetailAudio* detail = new CStreamDetailAudio;
+ detail->m_iChannels = resource->m_NbAudioChannels;
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ return NPT_SUCCESS;
+}
+
+std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ NPT_String ObjectClass = entry->m_ObjectClass.type.ToLowercase();
+
+ CFileItemPtr pItem(new CFileItem((const char*)entry->m_Title));
+ pItem->SetLabelPreformatted(true);
+ pItem->m_strTitle = (const char*)entry->m_Title;
+ pItem->m_bIsFolder = entry->IsContainer();
+
+ // if it's a container, format a string as upnp://uuid/object_id
+ if (pItem->m_bIsFolder) {
+
+ // look for metadata
+ if( ObjectClass.StartsWith("object.container.album.videoalbum") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, NULL, upnp_service);
+
+ } else if( ObjectClass.StartsWith("object.container.album.photoalbum")) {
+ //CPictureInfoTag* tag = pItem->GetPictureInfoTag();
+
+ } else if( ObjectClass.StartsWith("object.container.album") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, NULL, upnp_service);
+ }
+
+ } else {
+ bool audio = false
+ , image = false
+ , video = false;
+ // set a general content type
+ const char* content = NULL;
+ if (ObjectClass.StartsWith("object.item.videoitem")) {
+ pItem->SetMimeType("video/octet-stream");
+ content = "video";
+ video = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.audioitem")) {
+ pItem->SetMimeType("audio/octet-stream");
+ content = "audio";
+ audio = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.imageitem")) {
+ pItem->SetMimeType("image/octet-stream");
+ content = "image";
+ image = true;
+ }
+
+ // attempt to find a valid resource (may be multiple)
+ PLT_MediaItemResource resource, *res = NULL;
+ if(NPT_SUCCEEDED(NPT_ContainerFind(entry->m_Resources,
+ CResourceFinder("http-get", content), resource))) {
+
+ // set metadata
+ if (resource.m_Size != (NPT_LargeSize)-1) {
+ pItem->m_dwSize = resource.m_Size;
+ }
+ res = &resource;
+ }
+ // look for metadata
+ if(video) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, res, upnp_service);
+
+ } else if(audio) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, res, upnp_service);
+
+ } else if(image) {
+ //! @todo fill pictureinfotag?
+ GetResource(entry, *pItem);
+ }
+ }
+
+ // look for date?
+ if(entry->m_Description.date.GetLength()) {
+ KODI::TIME::SystemTime time = {};
+ sscanf(entry->m_Description.date, "%hu-%hu-%huT%hu:%hu:%hu", &time.year, &time.month, &time.day,
+ &time.hour, &time.minute, &time.second);
+ pItem->m_dateTime = time;
+ }
+
+ // if there is a thumbnail available set it here
+ if(entry->m_ExtraInfo.album_arts.GetItem(0))
+ // only considers first album art
+ pItem->SetArt("thumb", (const char*) entry->m_ExtraInfo.album_arts.GetItem(0)->uri);
+ else if(entry->m_Description.icon_uri.GetLength())
+ pItem->SetArt("thumb", (const char*) entry->m_Description.icon_uri);
+
+ for (unsigned int index = 0; index < entry->m_XbmcInfo.artwork.GetItemCount(); index++)
+ pItem->SetArt(entry->m_XbmcInfo.artwork.GetItem(index)->type.GetChars(),
+ entry->m_XbmcInfo.artwork.GetItem(index)->url.GetChars());
+
+ // set the watched overlay, as this will not be set later due to
+ // content set on file item list
+ if (pItem->HasVideoInfoTag()) {
+ int episodes = pItem->GetVideoInfoTag()->m_iEpisode;
+ int played = pItem->GetVideoInfoTag()->GetPlayCount();
+ const std::string& type = pItem->GetVideoInfoTag()->m_type;
+ bool watched(false);
+ if (type == MediaTypeTvShow || type == MediaTypeSeason) {
+ pItem->SetProperty("totalepisodes", episodes);
+ pItem->SetProperty("numepisodes", episodes);
+ pItem->SetProperty("watchedepisodes", played);
+ pItem->SetProperty("unwatchedepisodes", episodes - played);
+ pItem->SetProperty("watchedepisodepercent", episodes > 0 ? played * 100 / episodes : 0);
+ watched = (episodes && played >= episodes);
+ pItem->GetVideoInfoTag()->SetPlayCount(watched ? 1 : 0);
+ }
+ else if (type == MediaTypeEpisode || type == MediaTypeMovie)
+ watched = (played > 0);
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, watched);
+ }
+ return pItem;
+}
+
+struct ResourcePrioritySort
+{
+ explicit ResourcePrioritySort(const PLT_MediaObject* entry)
+ {
+ if (entry->m_ObjectClass.type.StartsWith("object.item.audioItem"))
+ m_content = "audio";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.imageItem"))
+ m_content = "image";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.videoItem"))
+ m_content = "video";
+ }
+
+ int GetPriority(const PLT_MediaItemResource& res) const
+ {
+ int prio = 0;
+
+ if (m_content != "" && res.m_ProtocolInfo.GetContentType().StartsWith(m_content))
+ prio += 400;
+
+ NPT_Url url(res.m_Uri);
+ if (URIUtils::IsHostOnLAN((const char*)url.GetHost(), false))
+ prio += 300;
+
+ if (res.m_ProtocolInfo.GetProtocol() == "xbmc-get")
+ prio += 200;
+ else if (res.m_ProtocolInfo.GetProtocol() == "http-get")
+ prio += 100;
+
+ return prio;
+ }
+
+ int operator()(const PLT_MediaItemResource& lh, const PLT_MediaItemResource& rh) const
+ {
+ if(GetPriority(lh) < GetPriority(rh))
+ return 1;
+ else
+ return 0;
+ }
+
+ NPT_String m_content;
+};
+
+bool GetResource(const PLT_MediaObject* entry, CFileItem& item)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("CUPnPDirectory::GetResource");
+
+ PLT_MediaItemResource resource;
+
+ // store original path so we remember it
+ item.SetProperty("original_listitem_url", item.GetPath());
+ item.SetProperty("original_listitem_mime", item.GetMimeType());
+
+ // get a sorted list based on our preference
+ NPT_List<PLT_MediaItemResource> sorted;
+ for (NPT_Cardinal i = 0; i < entry->m_Resources.GetItemCount(); ++i) {
+ sorted.Add(entry->m_Resources[i]);
+ }
+ sorted.Sort(ResourcePrioritySort(entry));
+
+ if(sorted.GetItemCount() == 0)
+ return false;
+
+ resource = *sorted.GetFirstItem();
+
+ // if it's an item, path is the first url to the item
+ // we hope the server made the first one reachable for us
+ // (it could be a format we dont know how to play however)
+ item.SetDynPath((const char*) resource.m_Uri);
+
+ // look for content type in protocol info
+ if (resource.m_ProtocolInfo.IsValid()) {
+ logger->debug("resource protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+
+ if (resource.m_ProtocolInfo.GetContentType().Compare("application/octet-stream") != 0) {
+ item.SetMimeType((const char*)resource.m_ProtocolInfo.GetContentType());
+ }
+
+ // if this is an image fill the thumb of the item
+ if (StringUtils::StartsWithNoCase(resource.m_ProtocolInfo.GetContentType(), "image"))
+ {
+ item.SetArt("thumb", std::string(resource.m_Uri));
+ }
+ } else {
+ logger->error("invalid protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+ }
+
+ // look for subtitles
+ unsigned subIdx = 0;
+
+ for(unsigned r = 0; r < entry->m_Resources.GetItemCount(); r++)
+ {
+ const PLT_MediaItemResource& res = entry->m_Resources[r];
+ const PLT_ProtocolInfo& info = res.m_ProtocolInfo;
+
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == info.GetContentType().Split("/").GetLastItem()->GetChars())
+ {
+ ++subIdx;
+ logger->info("adding subtitle: #{}, type '{}', URI '{}'", subIdx, type,
+ res.m_Uri.GetChars());
+
+ std::string prop = StringUtils::Format("subtitle:{}", subIdx);
+ item.SetProperty(prop, (const char*)res.m_Uri);
+ }
+ }
+ }
+ return true;
+}
+
+std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta)
+{
+ PLT_MediaObjectListReference list;
+ PLT_MediaObject* object = NULL;
+ CFileItemPtr item;
+
+ if (NPT_SUCCEEDED(PLT_Didl::FromDidl(meta, list))) {
+ list->Get(0, object);
+ }
+
+ if (object) {
+ item = BuildObject(object);
+ }
+
+ if (item) {
+ item->SetPath((const char*)uri);
+ GetResource(object, *item);
+ } else {
+ item.reset(new CFileItem((const char*)uri, false));
+ }
+ return item;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPInternal.h b/xbmc/network/upnp/UPnPInternal.h
new file mode 100644
index 0000000..5723e22
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.h
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <Neptune/Source/Core/NptReferences.h>
+#include <Neptune/Source/Core/NptStrings.h>
+#include <Neptune/Source/Core/NptTypes.h>
+
+class CUPnPServer;
+class CFileItem;
+class CThumbLoader;
+class PLT_DeviceData;
+class PLT_HttpRequestContext;
+class PLT_MediaItemResource;
+class PLT_MediaObject;
+class NPT_String;
+namespace MUSIC_INFO {
+ class CMusicInfoTag;
+}
+class CVideoInfoTag;
+
+namespace UPNP
+{
+ enum UPnPService {
+ UPnPServiceNone = 0,
+ UPnPClient,
+ UPnPContentDirectory,
+ UPnPPlayer,
+ UPnPRenderer
+ };
+
+ class CResourceFinder {
+ public:
+ CResourceFinder(const char* protocol, const char* content = NULL);
+ bool operator()(const PLT_MediaItemResource& resource) const;
+ private:
+ NPT_String m_Protocol;
+ NPT_String m_Content;
+ };
+
+ enum EClientQuirks
+ {
+ ECLIENTQUIRKS_NONE = 0x0
+
+ /* Client requires folder's to be marked as storageFolders as vendor type (360)*/
+ , ECLIENTQUIRKS_ONLYSTORAGEFOLDER = 0x01
+
+ /* Client can't handle subtypes for videoItems (360) */
+ , ECLIENTQUIRKS_BASICVIDEOCLASS = 0x02
+
+ /* Client requires album to be set to [Unknown Series] to show title (WMP) */
+ , ECLIENTQUIRKS_UNKNOWNSERIES = 0x04
+ };
+
+ EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context);
+
+ enum EMediaControllerQuirks
+ {
+ EMEDIACONTROLLERQUIRKS_NONE = 0x00
+
+ /* Media Controller expects MIME type video/x-mkv instead of video/x-matroska (Samsung) */
+ , EMEDIACONTROLLERQUIRKS_X_MKV = 0x01
+ };
+
+ EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device);
+
+ const char* GetMimeTypeFromExtension(const char* extension, const PLT_HttpRequestContext* context = NULL);
+ NPT_String GetMimeType(const CFileItem& item, const PLT_HttpRequestContext* context = NULL);
+ NPT_String GetMimeType(const char* filename, const PLT_HttpRequestContext* context = NULL);
+ const NPT_String GetProtocolInfo(const CFileItem& item, const char* protocol, const PLT_HttpRequestContext* context = NULL);
+
+
+ const std::string& CorrectAllItemsSortHack(const std::string &item);
+
+ NPT_Result PopulateTagFromObject(MUSIC_INFO::CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource = NULL,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource = NULL,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateObjectFromTag(MUSIC_INFO::CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service = UPnPServiceNone);
+
+ PLT_MediaObject* BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context = NULL,
+ CUPnPServer* upnp_server = NULL,
+ UPnPService upnp_service = UPnPServiceNone);
+
+ std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service = UPnPServiceNone);
+
+ bool GetResource(const PLT_MediaObject* entry, CFileItem& item);
+ std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta);
+}
+
diff --git a/xbmc/network/upnp/UPnPPlayer.cpp b/xbmc/network/upnp/UPnPPlayer.cpp
new file mode 100644
index 0000000..0f0d52a
--- /dev/null
+++ b/xbmc/network/upnp/UPnPPlayer.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (c) 2006 elupus (Joakim Plate)
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPPlayer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "ThumbLoader.h"
+#include "UPnP.h"
+#include "UPnPInternal.h"
+#include "application/Application.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "music/MusicThumbLoader.h"
+#include "threads/Event.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoThumbLoader.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+#include <Platinum/Source/Devices/MediaRenderer/PltMediaController.h>
+#include <Platinum/Source/Devices/MediaServer/PltDidl.h>
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+using namespace std::chrono_literals;
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.player")
+
+namespace UPNP
+{
+
+class CUPnPPlayerController : public PLT_MediaControllerDelegate
+{
+public:
+ CUPnPPlayerController(PLT_MediaController* control,
+ PLT_DeviceDataReference& device,
+ IPlayerCallback& callback)
+ : m_control(control),
+ m_transport(NULL),
+ m_device(device),
+ m_instance(0),
+ m_callback(callback),
+ m_postime(0),
+ m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPPlayerController"))
+ {
+ m_posinfo = {};
+ m_device->FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", m_transport);
+ }
+
+ void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnSetAVTransportURIResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnPlayResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnStopResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
+ {
+ if(NPT_FAILED(res) || info == NULL)
+ m_logger->error("OnGetMediaInfoResult failed");
+ }
+
+ void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if(NPT_FAILED(res))
+ {
+ m_logger->error("OnGetTransportInfoResult failed");
+ m_trainfo.cur_speed = "0";
+ m_trainfo.cur_transport_state = "STOPPED";
+ m_trainfo.cur_transport_status = "ERROR_OCCURED";
+ }
+ else
+ m_trainfo = *info;
+ m_traevnt.Set();
+ }
+
+ void UpdatePositionInfo()
+ {
+ if(m_postime == 0
+ || m_postime > CTimeUtils::GetFrameTime())
+ return;
+
+ m_control->GetTransportInfo(m_device, m_instance, this);
+ m_control->GetPositionInfo(m_device, m_instance, this);
+ m_postime = 0;
+ }
+
+ void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if(NPT_FAILED(res) || info == NULL)
+ {
+ m_logger->error("OnGetMediaInfoResult failed");
+ m_posinfo = PLT_PositionInfo();
+ }
+ else
+ m_posinfo = *info;
+ m_postime = CTimeUtils::GetFrameTime() + 500;
+ m_posevnt.Set();
+ }
+
+
+ ~CUPnPPlayerController() override = default;
+
+ PLT_MediaController* m_control;
+ PLT_Service * m_transport;
+ PLT_DeviceDataReference m_device;
+ NPT_UInt32 m_instance;
+ IPlayerCallback& m_callback;
+
+ NPT_Result m_resstatus;
+ CEvent m_resevent;
+
+ CCriticalSection m_section;
+ unsigned int m_postime;
+
+ CEvent m_posevnt;
+ PLT_PositionInfo m_posinfo;
+
+ CEvent m_traevnt;
+ PLT_TransportInfo m_trainfo;
+
+ Logger m_logger;
+};
+
+CUPnPPlayer::CUPnPPlayer(IPlayerCallback& callback, const char* uuid)
+ : IPlayer(callback),
+ m_control(NULL),
+ m_delegate(NULL),
+ m_started(false),
+ m_stopremote(false),
+ m_logger(CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CUPnPPlayer[{}]", uuid)))
+{
+ m_control = CUPnP::GetInstance()->m_MediaController;
+
+ PLT_DeviceDataReference device;
+ if(NPT_SUCCEEDED(m_control->FindRenderer(uuid, device)))
+ {
+ m_delegate = new CUPnPPlayerController(m_control, device, callback);
+ CUPnP::RegisterUserdata(m_delegate);
+ }
+ else
+ m_logger->error("couldn't find device as {}", uuid);
+
+ CServiceBroker::GetWinSystem()->RegisterRenderLoop(this);
+}
+
+CUPnPPlayer::~CUPnPPlayer()
+{
+ CServiceBroker::GetWinSystem()->UnregisterRenderLoop(this);
+ CloseFile();
+ CUPnP::UnregisterUserdata(m_delegate);
+ delete m_delegate;
+}
+
+static NPT_Result WaitOnEvent(CEvent& event, XbmcThreads::EndTime<>& timeout)
+{
+ if (event.Wait(0ms))
+ return NPT_SUCCESS;
+
+ if (!CGUIDialogBusy::WaitOnEvent(event))
+ return NPT_FAILURE;
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPPlayer::PlayFile(const CFileItem& file,
+ const CPlayerOptions& options,
+ XbmcThreads::EndTime<>& timeout)
+{
+ CFileItem item(file);
+ NPT_Reference<CThumbLoader> thumb_loader;
+ NPT_Reference<PLT_MediaObject> obj;
+ NPT_String path(file.GetPath().c_str());
+ NPT_String tmp, resource;
+ EMediaControllerQuirks quirks = EMEDIACONTROLLERQUIRKS_NONE;
+
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+
+ if (file.IsVideoDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ else if (item.IsMusicDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+
+ obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer);
+ if(obj.IsNull()) goto failed;
+
+ NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed_todidl);
+ tmp.Insert(didl_header, 0);
+ tmp.Append(didl_footer);
+
+ quirks = GetMediaControllerQuirks(m_delegate->m_device.AsPointer());
+ if (quirks & EMEDIACONTROLLERQUIRKS_X_MKV)
+ {
+ for (NPT_Cardinal i=0; i< obj->m_Resources.GetItemCount(); i++) {
+ if (obj->m_Resources[i].m_ProtocolInfo.GetContentType().Compare("video/x-matroska") == 0) {
+ m_logger->debug("PlayFile({}): applying video/x-mkv quirk", file.GetPath());
+ NPT_String protocolInfo = obj->m_Resources[i].m_ProtocolInfo.ToString();
+ protocolInfo.Replace(":video/x-matroska:", ":video/x-mkv:");
+ obj->m_Resources[i].m_ProtocolInfo = PLT_ProtocolInfo(protocolInfo);
+ }
+ }
+ }
+
+ /* The resource uri's are stored in the Didl. We must choose the best resource
+ * for the playback device */
+ NPT_Cardinal res_index;
+ NPT_CHECK_LABEL_SEVERE(m_control->FindBestResource(m_delegate->m_device, *obj, res_index), failed_findbestresource);
+
+ // get the transport info to evaluate the TransportState to be able to
+ // determine whether we first need to call Stop()
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_gettransportinfo);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_gettransportinfo);
+
+ if (m_delegate->m_trainfo.cur_transport_state != "NO_MEDIA_PRESENT" &&
+ m_delegate->m_trainfo.cur_transport_state != "STOPPED")
+ {
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->Stop(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_stop);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_stop);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_stop);
+ }
+
+
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->SetAVTransportURI(m_delegate->m_device
+ , m_delegate->m_instance
+ , obj->m_Resources[res_index].m_Uri
+ , (const char*)tmp
+ , m_delegate), failed_setavtransporturi);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_setavtransporturi);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_setavtransporturi);
+
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->Play(m_delegate->m_device
+ , m_delegate->m_instance
+ , "1"
+ , m_delegate), failed_play);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_play);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_play);
+
+
+ /* wait for PLAYING state */
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ do {
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_waitplaying);
+
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_delegate->m_section);
+ if(m_delegate->m_trainfo.cur_transport_state == "PLAYING"
+ || m_delegate->m_trainfo.cur_transport_state == "PAUSED_PLAYBACK")
+ break;
+
+ if(m_delegate->m_trainfo.cur_transport_state == "STOPPED"
+ && m_delegate->m_trainfo.cur_transport_status != "OK")
+ {
+ m_logger->error("OpenFile({}): remote player signalled error", file.GetPath());
+ return NPT_FAILURE;
+ }
+ }
+
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_waitplaying);
+
+ } while(!timeout.IsTimePast());
+
+ if(options.starttime > 0)
+ {
+ /* many upnp units won't load file properly until after play (including xbmc) */
+ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device
+ , m_delegate->m_instance
+ , "REL_TIME"
+ , PLT_Didl::FormatTimeStamp((NPT_UInt32)options.starttime)
+ , m_delegate), failed_seek);
+ }
+
+ return NPT_SUCCESS;
+failed_todidl:
+ m_logger->error("PlayFile({}) failed to serialize item into DIDL-Lite", file.GetPath());
+ return NPT_FAILURE;
+failed_findbestresource:
+ m_logger->error("PlayFile({}) failed to find a matching resource", file.GetPath());
+ return NPT_FAILURE;
+failed_gettransportinfo:
+ m_logger->error("PlayFile({}): call to GetTransportInfo failed", file.GetPath());
+ return NPT_FAILURE;
+failed_stop:
+ m_logger->error("PlayFile({}) failed to stop current playback", file.GetPath());
+ return NPT_FAILURE;
+failed_setavtransporturi:
+ m_logger->error("PlayFile({}) failed to set the playback URI", file.GetPath());
+ return NPT_FAILURE;
+failed_play:
+ m_logger->error("PlayFile({}) failed to start playback", file.GetPath());
+ return NPT_FAILURE;
+failed_waitplaying:
+ m_logger->error("PlayFile({}) failed to wait for PLAYING state", file.GetPath());
+ return NPT_FAILURE;
+failed_seek:
+ m_logger->error("PlayFile({}) failed to seek to start offset", file.GetPath());
+ return NPT_FAILURE;
+failed:
+ m_logger->error("PlayFile({}) failed", file.GetPath());
+ return NPT_FAILURE;
+}
+
+bool CUPnPPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options)
+{
+ XbmcThreads::EndTime<> timeout(10s);
+
+ /* if no path we want to attach to a already playing player */
+ if(file.GetPath() == "")
+ {
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed);
+
+ /* make sure the attached player is actually playing */
+ {
+ std::unique_lock<CCriticalSection> lock(m_delegate->m_section);
+ if(m_delegate->m_trainfo.cur_transport_state != "PLAYING"
+ && m_delegate->m_trainfo.cur_transport_state != "PAUSED_PLAYBACK")
+ goto failed;
+ }
+ }
+ else
+ NPT_CHECK_LABEL_SEVERE(PlayFile(file, options, timeout), failed);
+
+ m_stopremote = true;
+ m_started = true;
+ m_callback.OnPlayBackStarted(file);
+ m_callback.OnAVStarted(file);
+ NPT_CHECK_LABEL_SEVERE(m_control->GetPositionInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ NPT_CHECK_LABEL_SEVERE(m_control->GetMediaInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+
+ m_updateTimer.Set(0ms);
+
+ return true;
+failed:
+ m_logger->error("OpenFile({}) failed to open file", file.GetPath());
+ return false;
+}
+
+bool CUPnPPlayer::QueueNextFile(const CFileItem& file)
+{
+ CFileItem item(file);
+ NPT_Reference<CThumbLoader> thumb_loader;
+ NPT_Reference<PLT_MediaObject> obj;
+ NPT_String path(file.GetPath().c_str());
+ NPT_String tmp;
+
+ if (file.IsVideoDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ else if (item.IsMusicDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+
+
+ obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer);
+ if(!obj.IsNull())
+ {
+ NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed);
+ tmp.Insert(didl_header, 0);
+ tmp.Append(didl_footer);
+ }
+
+ NPT_CHECK_LABEL_WARNING(m_control->SetNextAVTransportURI(m_delegate->m_device
+ , m_delegate->m_instance
+ , file.GetPath().c_str()
+ , (const char*)tmp
+ , m_delegate), failed);
+ if (!m_delegate->m_resevent.Wait(10000ms))
+ goto failed;
+ NPT_CHECK_LABEL_WARNING(m_delegate->m_resstatus, failed);
+ return true;
+
+failed:
+ m_logger->error("QueueNextFile({}) failed to queue file", file.GetPath());
+ return false;
+}
+
+bool CUPnPPlayer::CloseFile(bool reopen)
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ if(m_stopremote)
+ {
+ NPT_CHECK_LABEL(m_control->Stop(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ if (!m_delegate->m_resevent.Wait(10000ms))
+ goto failed;
+ NPT_CHECK_LABEL(m_delegate->m_resstatus, failed);
+ }
+
+ if(m_started)
+ {
+ m_started = false;
+ m_callback.OnPlayBackStopped();
+ }
+
+ return true;
+failed:
+ m_logger->error("CloseFile - unable to stop playback");
+ return false;
+}
+
+void CUPnPPlayer::Pause()
+{
+ if(IsPaused())
+ {
+ NPT_CHECK_LABEL(m_control->Play(m_delegate->m_device
+ , m_delegate->m_instance
+ , "1"
+ , m_delegate), failed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, 1.0);
+ }
+ else
+ {
+ NPT_CHECK_LABEL(m_control->Pause(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, 0.0);
+ }
+
+ return;
+failed:
+ m_logger->error("CloseFile - unable to pause/unpause playback");
+}
+
+void CUPnPPlayer::SeekTime(int64_t ms)
+{
+ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device
+ , m_delegate->m_instance
+ , "REL_TIME", PLT_Didl::FormatTimeStamp((NPT_UInt32)(ms / 1000))
+ , m_delegate), failed);
+
+ CDataCacheCore::GetInstance().SeekFinished(0);
+ return;
+failed:
+ m_logger->error("SeekTime - unable to seek playback");
+}
+
+float CUPnPPlayer::GetPercentage()
+{
+ int64_t tot = GetTotalTime();
+ if(tot)
+ return 100.0f * GetTime() / tot;
+ else
+ return 0.0f;
+}
+
+void CUPnPPlayer::SeekPercentage(float percent)
+{
+ int64_t tot = GetTotalTime();
+ if (tot)
+ SeekTime((int64_t)(tot * percent / 100));
+}
+
+void CUPnPPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+}
+
+void CUPnPPlayer::DoAudioWork()
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ m_delegate->UpdatePositionInfo();
+
+ if(m_started) {
+ NPT_String uri, meta;
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackURI", uri), failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackMetadata", meta), failed);
+
+ if(m_current_uri != (const char*)uri
+ || m_current_meta != (const char*)meta) {
+ m_current_uri = (const char*)uri;
+ m_current_meta = (const char*)meta;
+ CFileItemPtr item = GetFileItem(uri, meta);
+ g_application.CurrentFileItem() = *item;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 0, -1,
+ static_cast<void*>(new CFileItem(*item)));
+ }
+
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ if(data == "STOPPED")
+ {
+ m_started = false;
+ m_callback.OnPlayBackEnded();
+ }
+ }
+ return;
+failed:
+ return;
+}
+
+bool CUPnPPlayer::IsPlaying() const
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ return data != "STOPPED";
+failed:
+ return false;
+}
+
+bool CUPnPPlayer::IsPaused() const
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ return data == "PAUSED_PLAYBACK";
+failed:
+ return false;
+}
+
+void CUPnPPlayer::SetVolume(float volume)
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_control->SetVolume(m_delegate->m_device
+ , m_delegate->m_instance
+ , "Master", (int)(volume * 100)
+ , m_delegate), failed);
+ return;
+failed:
+ m_logger->error("- unable to set volume");
+}
+
+int64_t CUPnPPlayer::GetTime()
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ return m_delegate->m_posinfo.rel_time.ToMillis();
+failed:
+ return 0;
+}
+
+int64_t CUPnPPlayer::GetTotalTime()
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ return m_delegate->m_posinfo.track_duration.ToMillis();
+failed:
+ return 0;
+};
+
+bool CUPnPPlayer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_STOP:
+ if(IsPlaying())
+ {
+ //stop on remote system
+ m_stopremote = HELPERS::ShowYesNoDialogText(CVariant{37022}, CVariant{37023}) ==
+ DialogResponse::CHOICE_YES;
+
+ return false; /* let normal code handle the action */
+ }
+ [[fallthrough]];
+ default:
+ return false;
+ }
+}
+
+void CUPnPPlayer::SetSpeed(float speed)
+{
+
+}
+
+void CUPnPPlayer::FrameMove()
+{
+ if (m_updateTimer.IsTimePast())
+ {
+ CDataCacheCore::GetInstance().SetPlayTimes(0, GetTime(), 0, GetTotalTime());
+ m_updateTimer.Set(500ms);
+ }
+}
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPPlayer.h b/xbmc/network/upnp/UPnPPlayer.h
new file mode 100644
index 0000000..aafd8c4
--- /dev/null
+++ b/xbmc/network/upnp/UPnPPlayer.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2006 elupus (Joakim Plate)
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "cores/IPlayer.h"
+#include "guilib/DispResource.h"
+#include "threads/SystemClock.h"
+#include "utils/logtypes.h"
+
+#include <string>
+
+class PLT_MediaController;
+
+namespace UPNP
+{
+
+class CUPnPPlayerController;
+
+class CUPnPPlayer
+ : public IPlayer, public IRenderLoop
+{
+public:
+ CUPnPPlayer(IPlayerCallback& callback, const char* uuid);
+ ~CUPnPPlayer() override;
+
+ bool OpenFile(const CFileItem& file, const CPlayerOptions& options) override;
+ bool QueueNextFile(const CFileItem &file) override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ void Pause() override;
+ bool HasVideo() const override { return false; }
+ bool HasAudio() const override { return false; }
+ void Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) override;
+ void SeekPercentage(float fPercent = 0) override;
+ void SetVolume(float volume) override;
+
+ int SeekChapter(int iChapter) override { return -1; }
+
+ void SeekTime(int64_t iTime = 0) override;
+ void SetSpeed(float speed = 0) override;
+
+ bool IsCaching() const override { return false; }
+ int GetCacheLevel() const override { return -1; }
+ void DoAudioWork() override;
+ bool OnAction(const CAction &action) override;
+
+ void FrameMove() override;
+
+ int PlayFile(const CFileItem& file,
+ const CPlayerOptions& options,
+ XbmcThreads::EndTime<>& timeout);
+
+private:
+ bool IsPaused() const;
+ int64_t GetTime();
+ int64_t GetTotalTime();
+ float GetPercentage();
+
+ PLT_MediaController* m_control;
+ CUPnPPlayerController* m_delegate;
+ std::string m_current_uri;
+ std::string m_current_meta;
+ bool m_started;
+ bool m_stopremote;
+ XbmcThreads::EndTime<> m_updateTimer;
+
+ Logger m_logger;
+};
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPRenderer.cpp b/xbmc/network/upnp/UPnPRenderer.cpp
new file mode 100644
index 0000000..d296471
--- /dev/null
+++ b/xbmc/network/upnp/UPnPRenderer.cpp
@@ -0,0 +1,758 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPRenderer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnP.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <inttypes.h>
+#include <mutex>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.renderer")
+
+namespace UPNP
+{
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::CUPnPRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer::CUPnPRenderer(const char* friendly_name, bool show_ip /*= false*/,
+ const char* uuid /*= NULL*/, unsigned int port /*= 0*/)
+ : PLT_MediaRenderer(friendly_name, show_ip, uuid, port)
+{
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::~CUPnPRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer::~CUPnPRenderer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::SetupServices()
+{
+ NPT_CHECK(PLT_MediaRenderer::SetupServices());
+
+ // update what we can play
+ PLT_Service* service = NULL;
+ NPT_CHECK_FATAL(FindServiceByType("urn:schemas-upnp-org:service:ConnectionManager:1", service));
+ service->SetStateVariable("SinkProtocolInfo"
+ ,"http-get:*:*:*"
+ ",xbmc-get:*:*:*"
+ ",http-get:*:audio/mkv:*"
+ ",http-get:*:audio/mpegurl:*"
+ ",http-get:*:audio/mpeg:*"
+ ",http-get:*:audio/mpeg3:*"
+ ",http-get:*:audio/mp3:*"
+ ",http-get:*:audio/mp4:*"
+ ",http-get:*:audio/basic:*"
+ ",http-get:*:audio/midi:*"
+ ",http-get:*:audio/ulaw:*"
+ ",http-get:*:audio/ogg:*"
+ ",http-get:*:audio/DVI4:*"
+ ",http-get:*:audio/G722:*"
+ ",http-get:*:audio/G723:*"
+ ",http-get:*:audio/G726-16:*"
+ ",http-get:*:audio/G726-24:*"
+ ",http-get:*:audio/G726-32:*"
+ ",http-get:*:audio/G726-40:*"
+ ",http-get:*:audio/G728:*"
+ ",http-get:*:audio/G729:*"
+ ",http-get:*:audio/G729D:*"
+ ",http-get:*:audio/G729E:*"
+ ",http-get:*:audio/GSM:*"
+ ",http-get:*:audio/GSM-EFR:*"
+ ",http-get:*:audio/L8:*"
+ ",http-get:*:audio/L16:*"
+ ",http-get:*:audio/LPC:*"
+ ",http-get:*:audio/MPA:*"
+ ",http-get:*:audio/PCMA:*"
+ ",http-get:*:audio/PCMU:*"
+ ",http-get:*:audio/QCELP:*"
+ ",http-get:*:audio/RED:*"
+ ",http-get:*:audio/VDVI:*"
+ ",http-get:*:audio/ac3:*"
+ ",http-get:*:audio/webm:*"
+ ",http-get:*:audio/vorbis:*"
+ ",http-get:*:audio/speex:*"
+ ",http-get:*:audio/flac:*"
+ ",http-get:*:audio/x-flac:*"
+ ",http-get:*:audio/x-aiff:*"
+ ",http-get:*:audio/x-pn-realaudio:*"
+ ",http-get:*:audio/x-realaudio:*"
+ ",http-get:*:audio/x-wav:*"
+ ",http-get:*:audio/x-matroska:*"
+ ",http-get:*:audio/x-ms-wma:*"
+ ",http-get:*:audio/x-mpegurl:*"
+ ",http-get:*:application/x-shockwave-flash:*"
+ ",http-get:*:application/ogg:*"
+ ",http-get:*:application/sdp:*"
+ ",http-get:*:image/gif:*"
+ ",http-get:*:image/jpeg:*"
+ ",http-get:*:image/ief:*"
+ ",http-get:*:image/png:*"
+ ",http-get:*:image/tiff:*"
+ ",http-get:*:image/webp:*"
+ ",http-get:*:video/avi:*"
+ ",http-get:*:video/divx:*"
+ ",http-get:*:video/mpeg:*"
+ ",http-get:*:video/fli:*"
+ ",http-get:*:video/flv:*"
+ ",http-get:*:video/quicktime:*"
+ ",http-get:*:video/vnd.vivo:*"
+ ",http-get:*:video/vc1:*"
+ ",http-get:*:video/ogg:*"
+ ",http-get:*:video/mp4:*"
+ ",http-get:*:video/mkv:*"
+ ",http-get:*:video/BT656:*"
+ ",http-get:*:video/CelB:*"
+ ",http-get:*:video/JPEG:*"
+ ",http-get:*:video/H261:*"
+ ",http-get:*:video/H263:*"
+ ",http-get:*:video/H263-1998:*"
+ ",http-get:*:video/H263-2000:*"
+ ",http-get:*:video/MPV:*"
+ ",http-get:*:video/MP2T:*"
+ ",http-get:*:video/MP1S:*"
+ ",http-get:*:video/MP2P:*"
+ ",http-get:*:video/BMPEG:*"
+ ",http-get:*:video/webm:*"
+ ",http-get:*:video/xvid:*"
+ ",http-get:*:video/x-divx:*"
+ ",http-get:*:video/x-matroska:*"
+ ",http-get:*:video/x-mkv:*"
+ ",http-get:*:video/x-ms-wmv:*"
+ ",http-get:*:video/x-ms-avi:*"
+ ",http-get:*:video/x-flv:*"
+ ",http-get:*:video/x-fli:*"
+ ",http-get:*:video/x-ms-asf:*"
+ ",http-get:*:video/x-ms-asx:*"
+ ",http-get:*:video/x-ms-wmx:*"
+ ",http-get:*:video/x-ms-wvx:*"
+ ",http-get:*:video/x-msvideo:*"
+ ",http-get:*:video/x-xvid:*"
+ );
+
+ NPT_CHECK_FATAL(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetadata", "");
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::ProcessHttpRequest
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::ProcessHttpGetRequest(NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // get the address of who sent us some data back
+ NPT_String ip_address = context.GetRemoteAddress().GetIpAddress().ToString();
+ NPT_String method = request.GetMethod();
+ NPT_String protocol = request.GetProtocol();
+ NPT_HttpUrl url = request.GetUrl();
+
+ if (url.GetPath() == "/thumb") {
+ NPT_HttpUrlQuery query(url.GetQuery());
+ NPT_String filepath = query.GetField("path");
+ if (!filepath.IsEmpty()) {
+ NPT_HttpEntity* entity = response.GetEntity();
+ if (entity == NULL) return NPT_ERROR_INVALID_STATE;
+
+ // check the method
+ if (request.GetMethod() != NPT_HTTP_METHOD_GET &&
+ request.GetMethod() != NPT_HTTP_METHOD_HEAD) {
+ response.SetStatus(405, "Method Not Allowed");
+ return NPT_SUCCESS;
+ }
+
+ // prevent hackers from accessing files outside of our root
+ if ((filepath.Find("/..") >= 0) || (filepath.Find("\\..") >=0)) {
+ return NPT_FAILURE;
+ }
+
+ // open the file
+ std::string path (CURL::Decode((const char*) filepath));
+ NPT_File file(path.c_str());
+ NPT_Result result = file.Open(NPT_FILE_OPEN_MODE_READ);
+ if (NPT_FAILED(result)) {
+ response.SetStatus(404, "Not Found");
+ return NPT_SUCCESS;
+ }
+ NPT_InputStreamReference stream;
+ file.GetInputStream(stream);
+ entity->SetContentType(GetMimeType(filepath));
+ entity->SetInputStream(stream, true);
+
+ return NPT_SUCCESS;
+ }
+ }
+
+ return PLT_MediaRenderer::ProcessHttpGetRequest(request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::Announce
++---------------------------------------------------------------------*/
+void CUPnPRenderer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (sender != ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ NPT_AutoLock lock(m_state);
+ PLT_Service *avt, *rct;
+
+ if (flag == ANNOUNCEMENT::Player)
+ {
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", avt)))
+ return;
+
+ if (message == "OnPlay" || message == "OnResume")
+ {
+ avt->SetStateVariable("AVTransportURI", g_application.CurrentFile().c_str());
+ avt->SetStateVariable("CurrentTrackURI", g_application.CurrentFile().c_str());
+
+ NPT_String meta;
+ if (NPT_SUCCEEDED(GetMetadata(meta)))
+ {
+ avt->SetStateVariable("CurrentTrackMetadata", meta);
+ avt->SetStateVariable("AVTransportURIMetaData", meta);
+ }
+
+ avt->SetStateVariable("TransportPlaySpeed",
+ NPT_String::FromInteger(data["player"]["speed"].asInteger()));
+ avt->SetStateVariable("TransportState", "PLAYING");
+
+ /* this could be a transition to next track, so clear next */
+ avt->SetStateVariable("NextAVTransportURI", "");
+ avt->SetStateVariable("NextAVTransportURIMetaData", "");
+ }
+ else if (message == "OnPause")
+ {
+ int64_t speed = data["player"]["speed"].asInteger();
+ avt->SetStateVariable("TransportPlaySpeed", NPT_String::FromInteger(speed != 0 ? speed : 1));
+ avt->SetStateVariable("TransportState", "PAUSED_PLAYBACK");
+ }
+ else if (message == "OnSpeedChanged")
+ {
+ avt->SetStateVariable("TransportPlaySpeed",
+ NPT_String::FromInteger(data["player"]["speed"].asInteger()));
+ }
+ }
+ else if (flag == ANNOUNCEMENT::Application && message == "OnVolumeChanged")
+ {
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:RenderingControl:1", rct)))
+ return;
+
+ std::string buffer;
+
+ buffer = std::to_string(data["volume"].asInteger());
+ rct->SetStateVariable("Volume", buffer.c_str());
+
+ buffer = std::to_string(256 * (data["volume"].asInteger() * 60 - 60) / 100);
+ rct->SetStateVariable("VolumeDb", buffer.c_str());
+
+ rct->SetStateVariable("Mute", data["muted"].asBoolean() ? "1" : "0");
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::UpdateState
++---------------------------------------------------------------------*/
+void
+CUPnPRenderer::UpdateState()
+{
+ NPT_AutoLock lock(m_state);
+
+ PLT_Service *avt;
+
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", avt)))
+ return;
+
+ /* don't update state while transitioning */
+ NPT_String state;
+ avt->GetStateVariableValue("TransportState", state);
+ if(state == "TRANSITIONING")
+ return;
+
+ avt->SetStateVariable("TransportStatus", "OK");
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying() || appPlayer->IsPausedPlayback())
+ {
+ avt->SetStateVariable("NumberOfTracks", "1");
+ avt->SetStateVariable("CurrentTrack", "1");
+
+ // get elapsed time
+ std::string buffer = StringUtils::SecondsToTimeString(std::lrint(g_application.GetTime()),
+ TIME_FORMAT_HH_MM_SS);
+ avt->SetStateVariable("RelativeTimePosition", buffer.c_str());
+ avt->SetStateVariable("AbsoluteTimePosition", buffer.c_str());
+
+ // get duration
+ buffer = StringUtils::SecondsToTimeString(std::lrint(g_application.GetTotalTime()),
+ TIME_FORMAT_HH_MM_SS);
+ if (buffer.length() > 0)
+ {
+ avt->SetStateVariable("CurrentTrackDuration", buffer.c_str());
+ avt->SetStateVariable("CurrentMediaDuration", buffer.c_str());
+ }
+ else
+ {
+ avt->SetStateVariable("CurrentTrackDuration", "00:00:00");
+ avt->SetStateVariable("CurrentMediaDuration", "00:00:00");
+ }
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ avt->SetStateVariable("TransportState", "PLAYING");
+
+ const std::string filePath = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ SLIDESHOW_FILE_PATH, INFO::DEFAULT_CONTEXT);
+ avt->SetStateVariable("AVTransportURI", filePath.c_str());
+ avt->SetStateVariable("CurrentTrackURI", filePath.c_str());
+ avt->SetStateVariable("TransportPlaySpeed", "1");
+
+ CGUIWindowSlideShow* slideshow =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(
+ WINDOW_SLIDESHOW);
+ if (slideshow)
+ {
+ std::string index;
+ index = std::to_string(slideshow->NumSlides());
+ avt->SetStateVariable("NumberOfTracks", index.c_str());
+ index = std::to_string(slideshow->CurrentSlide());
+ avt->SetStateVariable("CurrentTrack", index.c_str());
+ }
+
+ avt->SetStateVariable("CurrentTrackMetadata", "");
+ avt->SetStateVariable("AVTransportURIMetaData", "");
+ }
+ else
+ {
+ avt->SetStateVariable("TransportState", "STOPPED");
+ avt->SetStateVariable("TransportPlaySpeed", "1");
+ avt->SetStateVariable("NumberOfTracks", "0");
+ avt->SetStateVariable("CurrentTrack", "0");
+ avt->SetStateVariable("RelativeTimePosition", "00:00:00");
+ avt->SetStateVariable("AbsoluteTimePosition", "00:00:00");
+ avt->SetStateVariable("CurrentTrackDuration", "00:00:00");
+ avt->SetStateVariable("CurrentMediaDuration", "00:00:00");
+ avt->SetStateVariable("NextAVTransportURI", "");
+ avt->SetStateVariable("NextAVTransportURIMetaData", "");
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::GetMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::GetMetadata(NPT_String& meta)
+{
+ NPT_Result res = NPT_FAILURE;
+ CFileItem item(g_application.CurrentFileItem());
+ NPT_String file_path, tmp;
+
+ // we pass an empty CThumbLoader reference, as it can't be used
+ // without CUPnPServer enabled
+ NPT_Reference<CThumbLoader> thumb_loader;
+ PLT_MediaObject* object = BuildObject(item, file_path, false, thumb_loader, NULL, NULL, UPnPRenderer);
+ if (object) {
+ // fetch the item's artwork
+ std::string thumb;
+ if (object->m_ObjectClass.type == "object.item.audioItem.musicTrack")
+ thumb = CServiceBroker::GetGUI()->GetInfoManager().GetImage(MUSICPLAYER_COVER, -1);
+ else
+ thumb = CServiceBroker::GetGUI()->GetInfoManager().GetImage(VIDEOPLAYER_COVER, -1);
+
+ thumb = CTextureUtils::GetWrappedImageURL(thumb);
+
+ NPT_String ip;
+ if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
+ ip = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
+ }
+ // build url, use the internal device http server to serv the image
+ NPT_HttpUrlQuery query;
+ query.AddField("path", thumb.c_str());
+ PLT_AlbumArtInfo art;
+ art.uri = NPT_HttpUrl(
+ ip,
+ m_URLDescription.GetPort(),
+ "/thumb",
+ query.ToString()).ToString();
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(item.GetArt("thumb"), ".png")) {
+ art.dlna_profile = "PNG_TN";
+ } else {
+ art.dlna_profile = "JPEG_TN";
+ }
+ object->m_ExtraInfo.album_arts.Add(art);
+
+ res = PLT_Didl::ToDidl(*object, "*", tmp);
+ meta = didl_header + tmp + didl_footer;
+ delete object;
+ }
+ return res;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnNext
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnNext(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_NEXT);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPause
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPause(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ }
+ else
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPausedPlayback())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPlay
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPlay(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ return NPT_SUCCESS;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPausedPlayback())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ else if (appPlayer && !appPlayer->IsPlaying())
+ {
+ NPT_String uri, meta;
+ PLT_Service* service;
+ // look for value set previously by SetAVTransportURI
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+ NPT_CHECK_SEVERE(service->GetStateVariableValue("AVTransportURI", uri));
+ NPT_CHECK_SEVERE(service->GetStateVariableValue("AVTransportURIMetaData", meta));
+
+ // if not set, use the current file being played
+ PlayMedia(uri, meta);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPrevious
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPrevious(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_PREV_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PREV);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnStop
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnStop(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetAVTransportURI
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetAVTransportURI(PLT_ActionReference& action)
+{
+ NPT_String uri, meta;
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ NPT_CHECK_SEVERE(action->GetArgumentValue("CurrentURI", uri));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("CurrentURIMetaData", meta));
+
+ // if not playing already, just keep around uri & metadata
+ // and wait for play command
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying() &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_SLIDESHOW)
+ {
+ service->SetStateVariable("TransportState", "STOPPED");
+ service->SetStateVariable("TransportStatus", "OK");
+ service->SetStateVariable("TransportPlaySpeed", "1");
+ service->SetStateVariable("AVTransportURI", uri);
+ service->SetStateVariable("AVTransportURIMetaData", meta);
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetaData", "");
+
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+ return NPT_SUCCESS;
+ }
+
+ return PlayMedia(uri, meta, action.AsPointer());
+}
+
+/*----------------------------------------------------------------------
+ | CUPnPRenderer::OnSetAVTransportURI
+ +---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetNextAVTransportURI(PLT_ActionReference& action)
+{
+ NPT_String uri, meta;
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ NPT_CHECK_SEVERE(action->GetArgumentValue("NextURI", uri));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("NextURIMetaData", meta));
+
+ CFileItemPtr item = GetFileItem(uri, meta);
+ if (!item) {
+ return NPT_FAILURE;
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+
+ PLAYLIST::Id playlistId = PLAYLIST::TYPE_MUSIC;
+ if (item->IsVideo())
+ playlistId = PLAYLIST::TYPE_VIDEO;
+
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Add(playlistId, item);
+
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(-1);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ }
+
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+
+ service->SetStateVariable("NextAVTransportURI", uri);
+ service->SetStateVariable("NextAVTransportURIMetaData", meta);
+
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+
+ return NPT_SUCCESS;
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ return NPT_FAILURE;
+ }
+ else
+ {
+ return NPT_FAILURE;
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::PlayMedia
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::PlayMedia(const NPT_String& uri, const NPT_String& meta, PLT_Action* action)
+{
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ { NPT_AutoLock lock(m_state);
+ service->SetStateVariable("TransportState", "TRANSITIONING");
+ service->SetStateVariable("TransportStatus", "OK");
+ }
+
+ CFileItemPtr item = GetFileItem(uri, meta);
+ if (!item) {
+ return NPT_FAILURE;
+ }
+
+ if (item->IsPicture()) {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PICTURE_SHOW, -1, -1, nullptr,
+ item->GetPath());
+ } else {
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(*item));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l));
+ }
+
+ // just return success because the play actions are asynchronous
+ NPT_AutoLock lock(m_state);
+ service->SetStateVariable("TransportState", "PLAYING");
+ service->SetStateVariable("TransportStatus", "OK");
+ service->SetStateVariable("AVTransportURI", uri);
+ service->SetStateVariable("AVTransportURIMetaData", meta);
+
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetaData", "");
+
+ if (action) {
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetVolume
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetVolume(PLT_ActionReference& action)
+{
+ NPT_String volume;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("DesiredVolume", volume));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume((float)strtod((const char*)volume, NULL));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetMute
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetMute(PLT_ActionReference& action)
+{
+ NPT_String mute;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("DesiredMute",mute));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if ((mute == "1") ^ appVolume->IsMuted())
+ appVolume->ToggleMute();
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSeek
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSeek(PLT_ActionReference& action)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying())
+ return NPT_ERROR_INVALID_STATE;
+
+ NPT_String unit, target;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("Unit", unit));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("Target", target));
+
+ if (!unit.Compare("REL_TIME"))
+ {
+ // converts target to seconds
+ NPT_UInt32 seconds;
+ NPT_CHECK_SEVERE(PLT_Didl::ParseTimeStamp(target, seconds));
+ g_application.SeekTime(seconds);
+ }
+
+ return NPT_SUCCESS;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPRenderer.h b/xbmc/network/upnp/UPnPRenderer.h
new file mode 100644
index 0000000..e2ebe4b
--- /dev/null
+++ b/xbmc/network/upnp/UPnPRenderer.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/IAnnouncer.h"
+
+#include <Platinum/Source/Devices/MediaRenderer/PltMediaRenderer.h>
+
+class CVariant;
+
+namespace UPNP
+{
+
+class CRendererReferenceHolder
+{
+public:
+ PLT_DeviceHostReference m_Device;
+};
+
+class CUPnPRenderer : public PLT_MediaRenderer
+ , public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CUPnPRenderer(const char* friendly_name,
+ bool show_ip = false,
+ const char* uuid = NULL,
+ unsigned int port = 0);
+
+ ~CUPnPRenderer() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+ void UpdateState();
+
+ // Http server handler
+ NPT_Result ProcessHttpGetRequest(NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response) override;
+
+ // AVTransport methods
+ NPT_Result OnNext(PLT_ActionReference& action) override;
+ NPT_Result OnPause(PLT_ActionReference& action) override;
+ NPT_Result OnPlay(PLT_ActionReference& action) override;
+ NPT_Result OnPrevious(PLT_ActionReference& action) override;
+ NPT_Result OnStop(PLT_ActionReference& action) override;
+ NPT_Result OnSeek(PLT_ActionReference& action) override;
+ NPT_Result OnSetAVTransportURI(PLT_ActionReference& action) override;
+ NPT_Result OnSetNextAVTransportURI(PLT_ActionReference& action) override;
+
+ // RenderingControl methods
+ NPT_Result OnSetVolume(PLT_ActionReference& action) override;
+ NPT_Result OnSetMute(PLT_ActionReference& action) override;
+
+private:
+ NPT_Result SetupServices() override;
+ NPT_Result SetupIcons() override;
+ NPT_Result GetMetadata(NPT_String& meta);
+ NPT_Result PlayMedia(const NPT_String& uri,
+ const NPT_String& meta,
+ PLT_Action* action = NULL);
+ NPT_Mutex m_state;
+};
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPServer.cpp b/xbmc/network/upnp/UPnPServer.cpp
new file mode 100644
index 0000000..8e8432e
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.cpp
@@ -0,0 +1,1388 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPServer.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoThumbLoader.h"
+#include "view/GUIViewState.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.server")
+
+using namespace ANNOUNCEMENT;
+using namespace XFILE;
+using KODI::UTILITY::CDigest;
+
+namespace UPNP
+{
+
+NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
+
+const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
+ "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
+ "musicdb://singles/" };
+
+const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
+ "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/" };
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CUPnPServer
++---------------------------------------------------------------------*/
+CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/)
+ : PLT_MediaConnect(friendly_name, false, uuid, port),
+ PLT_FileMediaConnectDelegate("/", "/"),
+ m_scanning(CMusicLibraryQueue::GetInstance().IsScanningLibrary() ||
+ CVideoLibraryQueue::GetInstance().IsScanningLibrary()),
+ m_logger(CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CUPnPServer[{}]", friendly_name)))
+{
+}
+
+CUPnPServer::~CUPnPServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ProcessGetSCPD
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
+ return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupServices()
+{
+ PLT_MediaConnect::SetupServices();
+ PLT_Service* service = NULL;
+ NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
+ if (service)
+ {
+ service->SetStateVariable("SearchCapabilities", "upnp:class");
+ service->SetStateVariable("SortCapabilities", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating,upnp:episodeCount,upnp:episodeSeason,xbmc:rating,xbmc:dateadded,xbmc:votes");
+ }
+
+ m_scanning = true;
+ OnScanCompleted(AudioLibrary);
+ m_scanning = true;
+ OnScanCompleted(VideoLibrary);
+
+ // now safe to start passing on new notifications
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnScanCompleted
++---------------------------------------------------------------------*/
+void
+CUPnPServer::OnScanCompleted(int type)
+{
+ if (type == AudioLibrary) {
+ for (const char* const audio_container : audio_containers)
+ UpdateContainer(audio_container);
+ }
+ else if (type == VideoLibrary) {
+ for (const char* const video_container : video_containers)
+ UpdateContainer(video_container);
+ }
+ else
+ return;
+ m_scanning = false;
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::UpdateContainer
++---------------------------------------------------------------------*/
+void
+CUPnPServer::UpdateContainer(const std::string& id)
+{
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
+ unsigned long count = 0;
+ if (itr != m_UpdateIDs.end())
+ count = ++itr->second.second;
+ m_UpdateIDs[id] = std::make_pair(true, count);
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::PropagateUpdates
++---------------------------------------------------------------------*/
+void
+CUPnPServer::PropagateUpdates()
+{
+ PLT_Service* service = NULL;
+ NPT_String current_ids;
+ std::string buffer;
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr;
+
+ if (m_scanning || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNPANNOUNCE))
+ return;
+
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
+
+ // we pause, and we must retain any changes which have not been
+ // broadcast yet
+ NPT_CHECK_LABEL(service->PauseEventing(), failed);
+ NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
+ buffer = (const char*)current_ids;
+ if (!buffer.empty())
+ buffer.append(",");
+
+ // only broadcast ids with modified bit set
+ for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
+ if (itr->second.first) {
+ buffer.append(StringUtils::Format("{},{},", itr->first, itr->second.second));
+ itr->second.first = false;
+ }
+ }
+
+ // set the value, Platinum will clear ContainerUpdateIDs after sending
+ NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
+ NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
+
+ service->PauseEventing(false);
+ return;
+
+failed:
+ // should attempt to start eventing on a failure
+ if (service) service->PauseEventing(false);
+ m_logger->error("Unable to propagate updates");
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildSafeResourceUri
++---------------------------------------------------------------------*/
+NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path)
+{
+ CURL url(file_path);
+ std::string md5;
+ std::string mapped_file_path(file_path);
+
+ // determine the filename to provide context to md5'd urls
+ std::string filename;
+ if (url.IsProtocol("image"))
+ {
+ filename = URIUtils::GetFileName(url.GetHostName());
+ // Remove trailing / for Platinum/Neptune to recognize the file extension and use the correct mime type when serving the image
+ URIUtils::RemoveSlashAtEnd(mapped_file_path);
+ }
+ else
+ filename = URIUtils::GetFileName(mapped_file_path);
+
+ filename = CURL::Encode(filename);
+ md5 = CDigest::Calculate(CDigest::Type::MD5, mapped_file_path);
+ md5 += "/" + filename;
+ { NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(md5.c_str(), mapped_file_path.c_str()));
+ }
+ return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Build
++---------------------------------------------------------------------*/
+PLT_MediaObject* CUPnPServer::Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const char* parent_id /* = NULL */)
+{
+ PLT_MediaObject* object = NULL;
+ NPT_String path = item->GetPath().c_str();
+
+ //HACK: temporary disabling count as it thrashes HDD
+ with_count = false;
+
+ m_logger->debug("Preparing upnp object for item '{}'", (const char*)path);
+
+ if (path.StartsWith("virtualpath://upnproot")) {
+ path.TrimRight("/");
+ item->m_bIsFolder = true;
+ if (path.StartsWith("virtualpath://")) {
+ object = new PLT_MediaContainer;
+ object->m_Title = item->GetLabel().c_str();
+ object->m_ObjectClass.type = "object.container";
+ object->m_ObjectID = path;
+
+ // root
+ object->m_ObjectID = "0";
+ object->m_ParentID = "-1";
+ // root has 5 children
+
+ //This is dead code because of the HACK a few lines up setting with_count to false
+ //if (with_count) {
+ // ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
+ //}
+ } else {
+ goto failure;
+ }
+
+ } else {
+ // db path handling
+ NPT_String file_path, share_name;
+ file_path = item->GetPath().c_str();
+ share_name = "";
+
+ if (path.StartsWith("musicdb://")) {
+ if (path == "musicdb://" ) {
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasMusicInfoTag()) {
+ MUSICDATABASEDIRECTORY::CQueryParams params;
+ MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CMusicDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetSongId() >= 0 ) {
+ CSong song;
+ if (db.GetSong(params.GetSongId(), song))
+ item->GetMusicInfoTag()->SetSong(song);
+ }
+ else if (params.GetAlbumId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CAlbum album;
+ if (db.GetAlbum(params.GetAlbumId(), album, false))
+ item->GetMusicInfoTag()->SetAlbum(album);
+ }
+ else if (params.GetArtistId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CArtist artist;
+ if (db.GetArtist(params.GetArtistId(), artist, false))
+ item->GetMusicInfoTag()->SetArtist(artist);
+ }
+ }
+
+ // all items appart from songs (artists, albums, etc) are folders
+ if (!item->HasMusicInfoTag() || item->GetMusicInfoTag()->GetType() != MediaTypeSong)
+ {
+ item->m_bIsFolder = true;
+ }
+
+ if (item->GetLabel().empty()) {
+ /* if no label try to grab it from node type */
+ std::string label;
+ if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
+ if (path == "library://video/" ) {
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasVideoInfoTag()) {
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CVideoDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetMovieId() >= 0 )
+ db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
+ else if (params.GetMVideoId() >= 0 )
+ db.GetMusicVideoInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMVideoId());
+ else if (params.GetEpisodeId() >= 0 )
+ db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
+ else if (params.GetTvShowId() >= 0)
+ {
+ if (params.GetSeason() >= 0)
+ {
+ int idSeason = db.GetSeasonId(params.GetTvShowId(), params.GetSeason());
+ if (idSeason >= 0)
+ db.GetSeasonInfo(idSeason, *item->GetVideoInfoTag());
+ }
+ else
+ db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
+ }
+ }
+
+ if (item->GetVideoInfoTag()->m_type == MediaTypeTvShow || item->GetVideoInfoTag()->m_type == MediaTypeSeason) {
+ // for tvshows and seasons, iEpisode and playCount are
+ // invalid
+ item->m_bIsFolder = true;
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
+ item->GetVideoInfoTag()->SetPlayCount(static_cast<int>(item->GetProperty("watchedepisodes").asInteger()));
+ }
+ // if this is an item in the library without a playable path it most be a folder
+ else if (item->GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ {
+ item->m_bIsFolder = true;
+ }
+
+ // try to grab title from tag
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty()) {
+ item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
+ item->SetLabelPreformatted(true);
+ }
+
+ // try to grab it from the folder
+ if (item->GetLabel().empty()) {
+ std::string label;
+ if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ }
+ // playlists are folders
+ else if (item->IsPlayList())
+ {
+ item->m_bIsFolder = true;
+ }
+ // audio and not a playlist -> song, so it's not a folder
+ else if (item->IsAudio())
+ {
+ item->m_bIsFolder = false;
+ }
+ // any other type of item -> delegate to CDirectory
+ else
+ {
+ item->m_bIsFolder = CDirectory::Exists(item->GetPath());
+ }
+
+ // not a virtual path directory, new system
+ object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this, UPnPContentDirectory);
+
+ // set parent id if passed, otherwise it should have been determined
+ if (object && parent_id) {
+ object->m_ParentID = parent_id;
+ }
+ }
+
+ if (object) {
+ // remap Root virtualpath://upnproot/ to id "0"
+ if (object->m_ObjectID == "virtualpath://upnproot/")
+ object->m_ObjectID = "0";
+
+ // remap Parent Root virtualpath://upnproot/ to id "0"
+ if (object->m_ParentID == "virtualpath://upnproot/")
+ object->m_ParentID = "0";
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Announce
++---------------------------------------------------------------------*/
+void CUPnPServer::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ NPT_String path;
+ int item_id;
+ std::string item_type;
+
+ if (sender != CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ if (message != "OnUpdate" && message != "OnRemove" && message != "OnScanStarted" &&
+ message != "OnScanFinished")
+ return;
+
+ if (data.isNull()) {
+ if (message == "OnScanStarted" || message == "OnCleanStarted")
+ {
+ m_scanning = true;
+ }
+ else if (message == "OnScanFinished" || message == "OnCleanFinished")
+ {
+ OnScanCompleted(flag);
+ }
+ }
+ else {
+ // handle both updates & removals
+ if (!data["item"].isNull()) {
+ item_id = (int)data["item"]["id"].asInteger();
+ item_type = data["item"]["type"].asString();
+ }
+ else {
+ item_id = (int)data["id"].asInteger();
+ item_type = data["type"].asString();
+ }
+
+ // we always update 'recently added' nodes along with the specific container,
+ // as we don't differentiate 'updates' from 'adds' in RPC interface
+ if (flag == VideoLibrary) {
+ if(item_type == MediaTypeEpisode) {
+ CVideoDatabase db;
+ if (!db.Open()) return;
+ int show_id = db.GetTvShowForEpisode(item_id);
+ int season_id = db.GetSeasonForEpisode(item_id);
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/", show_id));
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/{}/?tvshowid={}",
+ show_id, season_id, show_id));
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeTvShow) {
+ UpdateContainer("library://video/tvshows/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeMovie) {
+ UpdateContainer("library://video/movies/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmovies/");
+ }
+ else if(item_type == MediaTypeMusicVideo) {
+ UpdateContainer("library://video/musicvideos/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmusicvideos/");
+ }
+ }
+ else if (flag == AudioLibrary && item_type == MediaTypeSong) {
+ // we also update the 'songs' container is maybe a performance drop too
+ // high? would need to check if slow clients even cache at all anyway
+ CMusicDatabase db;
+ CAlbum album;
+ if (!db.Open()) return;
+ if (db.GetAlbumFromSong(item_id, album)) {
+ UpdateContainer(StringUtils::Format("musicdb://albums/{}", album.idAlbum));
+ UpdateContainer("musicdb://songs/");
+ UpdateContainer("musicdb://recentlyaddedalbums/");
+ }
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| TranslateWMPObjectId
++---------------------------------------------------------------------*/
+static NPT_String TranslateWMPObjectId(NPT_String id, const Logger& logger)
+{
+ if (id == "0") {
+ id = "virtualpath://upnproot/";
+ } else if (id == "15") {
+ // Xbox 360 asking for videos
+ id = "library://video/";
+ } else if (id == "16") {
+ // Xbox 360 asking for photos
+ } else if (id == "107") {
+ // Sonos uses 107 for artists root container id
+ id = "musicdb://artists/";
+ } else if (id == "7") {
+ // Sonos uses 7 for albums root container id
+ id = "musicdb://albums/";
+ } else if (id == "4") {
+ // Sonos uses 4 for tracks root container id
+ id = "musicdb://songs/";
+ }
+
+ logger->debug("Translated id to '{}'", (const char*)id);
+ return id;
+}
+
+NPT_Result
+ObjectIDValidate(const NPT_String& id)
+{
+ if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
+ return NPT_SUCCESS;
+ return NPT_ERROR_NO_SUCH_FILE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+ NPT_COMPILER_UNUSED(requested_count);
+ NPT_COMPILER_UNUSED(starting_index);
+
+ NPT_String didl;
+ NPT_Reference<PLT_MediaObject> object;
+ NPT_String id = TranslateWMPObjectId(object_id, m_logger);
+ CFileItemPtr item;
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ m_logger->info("Received UPnP Browse Metadata request for object '{}'", object_id);
+
+ if(NPT_FAILED(ObjectIDValidate(id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ if (id.StartsWith("virtualpath://")) {
+ id.TrimRight("/");
+ if (id == "virtualpath://upnproot") {
+ id += "/";
+ item.reset(new CFileItem((const char*)id, true));
+ item->SetLabel("Root");
+ item->SetLabelPreformatted(true);
+ object = Build(item, true, context, thumb_loader);
+ object->m_ParentID = "-1";
+ } else {
+ return NPT_FAILURE;
+ }
+ } else {
+ item.reset(new CFileItem((const char*)id, false));
+
+ // attempt to determine the parent of this item
+ std::string parent;
+ if (URIUtils::IsVideoDb((const char*)id) || URIUtils::IsMusicDb((const char*)id) || StringUtils::StartsWithNoCase((const char*)id, "library://video/")) {
+ if (!URIUtils::GetParentPath((const char*)id, parent)) {
+ parent = "0";
+ }
+ }
+ else {
+ // non-library objects - playlists / sources
+ //
+ // we could instead store the parents in a hash during every browse
+ // or could handle this in URIUtils::GetParentPath() possibly,
+ // however this is quicker to implement and subsequently purge when a
+ // better solution presents itself
+ std::string child_id((const char*)id);
+ if (StringUtils::StartsWithNoCase(child_id, "special://musicplaylists/")) parent = "musicdb://";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://videoplaylists/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "sources://video/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/music/")) parent = "special://musicplaylists/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/video/")) parent = "special://videoplaylists/";
+ else parent = "sources://video/"; // this can only match video sources
+ }
+
+ if (item->IsVideoDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (item->IsMusicDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+ object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
+ }
+
+ if (object.IsNull()) {
+ /* error */
+ NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
+ action->SetError(701, "No Such Object.");
+ return NPT_FAILURE;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ /* add didl header and footer */
+ didl = didl_header + tmp + didl_footer;
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
+
+ // update ID may be wrong here, it should be the one of the container?
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+
+ //! @todo We need to keep track of the overall SystemUpdateID of the CDS
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseDirectChildren
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ CFileItemList items;
+ NPT_String parent_id = TranslateWMPObjectId(object_id, m_logger);
+
+ m_logger->info("Received Browse DirectChildren request for object '{}', with sort criteria {}",
+ object_id, sort_criteria);
+
+ if(NPT_FAILED(ObjectIDValidate(parent_id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ items.SetPath(std::string(parent_id));
+
+ // guard against loading while saving to the same cache file
+ // as CArchive currently performs no locking itself
+ bool load;
+ { NPT_AutoLock lock(m_CacheMutex);
+ load = items.Load();
+ }
+
+ if (!load) {
+ // cache anything that takes more than a second to retrieve
+ auto start = std::chrono::steady_clock::now();
+
+ if (parent_id.StartsWith("virtualpath://upnproot")) {
+ CFileItemPtr item;
+
+ // music library
+ item.reset(new CFileItem("musicdb://", true));
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ // video library
+ item.reset(new CFileItem("library://video/", true));
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ items.Sort(SortByLabel, SortOrderAscending);
+ } else {
+ // this is the only way to hide unplayable items in the 'files'
+ // view as we cannot tell what context (eg music vs video) the
+ // request came from
+ std::string supported = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ CDirectory::GetDirectory((const char*)parent_id, items, supported, DIR_FLAG_DEFAULTS);
+ DefaultSortItems(items);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && duration.count() > 1000))
+ {
+ NPT_AutoLock lock(m_CacheMutex);
+ items.Save();
+ }
+ }
+
+ // as there's no library://music support, manually add playlists and music
+ // video nodes
+ if (items.GetPath() == "musicdb://") {
+ CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
+ playlists->SetLabel(g_localizeStrings.Get(136));
+ items.Add(playlists);
+
+ CVideoDatabase database;
+ database.Open();
+ if (database.HasContent(VideoDbContentType::MUSICVIDEOS))
+ {
+ CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
+ mvideos->SetLabel(g_localizeStrings.Get(20389));
+ items.Add(mvideos);
+ }
+ }
+
+ // Don't pass parent_id if action is Search not BrowseDirectChildren, as
+ // we want the engine to determine the best parent id, not necessarily the one
+ // passed
+ NPT_String action_name = action->GetActionDesc().GetName();
+ return BuildResponse(
+ action,
+ items,
+ filter,
+ starting_index,
+ requested_count,
+ sort_criteria,
+ context,
+ (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildResponse
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+
+ m_logger->debug("Building UPnP response with filter '{}', starting @ {} with {} requested",
+ filter, starting_index, requested_count);
+
+ // we will reuse this ThumbLoader for all items
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ if (URIUtils::IsVideoDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (URIUtils::IsMusicDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+
+ // this isn't pretty but needed to properly hide the addons node from clients
+ if (StringUtils::StartsWith(items.GetPath(), "library")) {
+ for (int i=0; i<items.Size(); i++) {
+ if (StringUtils::StartsWith(items[i]->GetPath(), "addons") ||
+ StringUtils::EndsWith(items[i]->GetPath(), "/addons.xml/"))
+ items.Remove(i);
+ }
+ }
+
+ // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
+ // 0 requested means as many as possible
+ NPT_UInt32 max_count = (requested_count == 0)?m_MaxReturnedItems:std::min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
+ NPT_UInt32 stop_index = std::min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
+
+ NPT_Cardinal count = 0;
+ NPT_Cardinal total = items.Size();
+ NPT_String didl = didl_header;
+ PLT_MediaObjectReference object;
+ for (unsigned long i=starting_index; i<stop_index; ++i) {
+ object = Build(items[i], true, context, thumb_loader, parent_id);
+ if (object.IsNull()) {
+ // don't tell the client this item ever existed
+ --total;
+ continue;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ // Neptunes string growing is dead slow for small additions
+ if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
+ didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
+ }
+ didl += tmp;
+ ++count;
+ }
+
+ didl += didl_footer;
+
+ m_logger->debug("Returning UPnP response with {} items out of {} total matches", count, total);
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| FindSubCriteria
++---------------------------------------------------------------------*/
+static
+NPT_String
+FindSubCriteria(NPT_String criteria, const char* name)
+{
+ NPT_String result;
+ int search = criteria.Find(name);
+ if (search >= 0) {
+ criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
+ criteria.TrimLeft(" ");
+ if (criteria.GetLength()>0 && criteria[0] == '=') {
+ criteria.TrimLeft("= ");
+ if (criteria.GetLength()>0 && criteria[0] == '\"') {
+ search = criteria.Find("\"", 1);
+ if (search > 0) result = criteria.SubString(1, search-1);
+ }
+ }
+ }
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnSearchContainer
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
+ const char* object_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ m_logger->debug("Received Search request for object '{}' with search '{}'", object_id,
+ search_criteria);
+
+ NPT_String id = object_id;
+ NPT_String searchClass = NPT_String(search_criteria);
+ if (id.StartsWith("musicdb://")) {
+ // we browse for all tracks given a genre, artist or album
+ if (searchClass.Find("object.item.audioItem") >= 0) {
+ if (!id.EndsWith("/")) id += "/";
+ NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
+ // remove extra empty node count
+ count = count?count-1:0;
+
+ // genre
+ if (id.StartsWith("musicdb://genres/")) {
+ // all tracks of all genres
+ if (count == 1)
+ id += "-1/-1/-1/";
+ // all tracks of a specific genre
+ else if (count == 2)
+ id += "-1/-1/";
+ // all tracks of a specific genre of a specific artist
+ else if (count == 3)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://artists/")) {
+ // all tracks by all artists
+ if (count == 1)
+ id += "-1/-1/";
+ // all tracks of a specific artist
+ else if (count == 2)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://albums/")) {
+ // all albums ?
+ if (count == 1) id += "-1/";
+ }
+ }
+ return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.item.audioItem") >= 0) {
+ // look for artist, album & genre filters
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ NPT_String album = FindSubCriteria(searchClass, "upnp:album");
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ // all tracks by genre filtered by artist and/or album
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist), // will return -1 if no artist
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ // all tracks by artist name filtered by album if passed
+ std::string strPath = StringUtils::Format(
+ "musicdb://artists/{}/{}/", database.GetArtistByName((const char*)artist),
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (album.GetLength() > 0) {
+ // all tracks by album name
+ std::string strPath = StringUtils::Format("musicdb://albums/{}/",
+ database.GetAlbumByName((const char*)album));
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+
+ // browse all songs
+ return OnBrowseDirectChildren(action, "musicdb://songs/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.musicAlbum") >= 0) {
+ // sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+
+ // 360 hack: artist/albums using search
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist)); // no artist should return -1
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ std::string strPath = StringUtils::Format("musicdb://artists/{}/",
+ database.GetArtistByName((const char*)artist));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ }
+
+ // all albums
+ return OnBrowseDirectChildren(action, "musicdb://albums/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.person.musicArtist") >= 0) {
+ // Sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ if (genre.GetLength() > 0) {
+ CMusicDatabase database;
+ database.Open();
+ std::string strPath = StringUtils::Format("musicdb://genres/{}/",
+ database.GetGenreByName((const char*)genre));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+ return OnBrowseDirectChildren(action, "musicdb://artists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.genre.musicGenre") >= 0) {
+ return OnBrowseDirectChildren(action, "musicdb://genres/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.playlistContainer") >= 0) {
+ return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastShow") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetTvShowsByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(),
+ items, SortDescription(), GetRequiredVideoDbDetails(NPT_String(filter)))) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastSeason") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetSeasonsByWhere("videodb://tvshows/titles/-1/?local", CDatabase::Filter(), items, true)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/-1/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.videoItem") >= 0) {
+ CFileItemList items, allItems;
+
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ bool allVideoItems = searchClass.Compare("object.item.videoItem") == 0;
+
+ // determine the required videodb details to be retrieved
+ int requiredVideoDbDetails = GetRequiredVideoDbDetails(NPT_String(filter));
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.movie") >= 0)
+ {
+ if (!database.GetMoviesByWhere("videodb://movies/titles/?local", CDatabase::Filter(), items, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.videoBroadcast") >= 0)
+ {
+ if (!database.GetEpisodesByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://tvshows/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.musicVideoClip") >= 0)
+ {
+ if (!database.GetMusicVideosByWhere("videodb://musicvideos/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://musicvideos/titles/");
+ }
+
+ if (allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+
+ return BuildResponse(action, allItems, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.imageItem") >= 0) {
+ CFileItemList items;
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ }
+
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnUpdateObject
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context)
+{
+ std::string path(CURL::Decode(object_id));
+ CFileItem updated;
+ updated.SetPath(path);
+ m_logger->info("OnUpdateObject: {} from {}", path,
+ (const char*)context.GetRemoteAddress().GetIpAddress().ToString());
+
+ NPT_String playCount, position, lastPlayed;
+ int err;
+ const char* msg = NULL;
+ bool updatelisting(false);
+
+ // we pause eventing as multiple announces may happen in this operation
+ PLT_Service* service = NULL;
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), error);
+ NPT_CHECK_LABEL(service->PauseEventing(), error);
+
+ if (updated.IsVideoDb()) {
+ CVideoDatabase db;
+ NPT_CHECK_LABEL(!db.Open(), error);
+
+ // must first determine type of file from object id
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
+
+ int id = -1;
+ VideoDbContentType content_type;
+ if ((id = params.GetMovieId()) >= 0 )
+ content_type = VideoDbContentType::MOVIES;
+ else if ((id = params.GetEpisodeId()) >= 0 )
+ content_type = VideoDbContentType::EPISODES;
+ else if ((id = params.GetMVideoId()) >= 0 )
+ content_type = VideoDbContentType::MUSICVIDEOS;
+ else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ std::string file_path;
+ db.GetFilePathById(id, file_path, content_type);
+ CVideoInfoTag tag;
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ m_logger->info("Translated to {}", file_path);
+
+ position = new_vals["lastPlaybackPosition"];
+ playCount = new_vals["playCount"];
+ lastPlayed = new_vals["lastPlaybackTime"];
+
+
+ if (!position.IsEmpty()
+ && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
+ NPT_UInt32 resume;
+ NPT_CHECK_LABEL(position.ToInteger32(resume), args);
+
+ if (resume <= 0)
+ db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
+ else {
+ CBookmark bookmark;
+ bookmark.timeInSeconds = resume;
+ bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
+ bookmark.playerState = new_vals["lastPlayerState"];
+
+ db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
+ }
+ if (playCount.IsEmpty()) {
+ CVariant data;
+ data["id"] = updated.GetVideoInfoTag()->m_iDbId;
+ data["type"] = updated.GetVideoInfoTag()->m_type;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnUpdate", data);
+ }
+ updatelisting = true;
+ }
+
+ if (!playCount.IsEmpty()
+ && playCount.Compare(current_vals["playCount"]) != 0) {
+
+ NPT_UInt32 count;
+ NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
+
+ CDateTime lastPlayedObj;
+ if (!lastPlayed.IsEmpty() &&
+ lastPlayed.Compare(current_vals["lastPlaybackTime"]) != 0)
+ lastPlayedObj.SetFromW3CDateTime(lastPlayed.GetChars());
+
+ db.SetPlayCount(updated, count, lastPlayedObj);
+ updatelisting = true;
+ }
+
+ // we must load the changed settings before propagating to local UI
+ if (updatelisting) {
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ //! TODO: we should find a way to avoid obtaining the artwork just to
+ // update the playcount or similar properties. Maybe a flag in the GUI
+ // update message to inform we should only update the playback properties
+ // without touching other parts of the item.
+ CVideoThumbLoader().FillLibraryArt(updated);
+ }
+
+ } else if (updated.IsMusicDb()) {
+ //! @todo implement this
+
+ } else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ if (updatelisting) {
+ updated.SetPath(path);
+ if (updated.IsVideoDb())
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ else if (updated.IsMusicDb())
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+
+ CFileItemPtr msgItem(new CFileItem(updated));
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, GUI_MSG_FLAG_UPDATE_LIST, msgItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ NPT_CHECK_LABEL(service->PauseEventing(false), error);
+ return NPT_SUCCESS;
+
+args:
+ err = 402;
+ msg = "Invalid args";
+ goto failure;
+
+error:
+ err = 501;
+ msg = "Internal error";
+
+failure:
+ m_logger->error("OnUpdateObject failed with err {}: {}", err, msg);
+ action->SetError(err, msg);
+ service->PauseEventing(false);
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ServeFile
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& md5)
+{
+ // Translate hash to filename
+ NPT_String file_path(md5), *file_path2;
+ { NPT_AutoLock lock(m_FileMutex);
+ if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
+ file_path = *file_path2;
+ m_logger->debug("Received request to serve '{}' = '{}'", (const char*)md5,
+ (const char*)file_path);
+ } else {
+ m_logger->debug("Received request to serve unknown md5 '{}'", (const char*)md5);
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+ }
+
+ // File requested
+ NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
+
+ if (file_path.Left(8).Compare("stack://", true) == 0) {
+
+ NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
+ if (files.GetItemCount() == 0) {
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+
+ NPT_String output;
+ output.Reserve(file_path.GetLength()*2);
+ output += "#EXTM3U\r\n";
+
+ NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
+ for (;url;url++) {
+ output += ("#EXTINF:-1," + URIUtils::GetFileName((const char*)*url)).c_str();
+ output += "\r\n";
+ output += BuildSafeResourceUri(
+ rooturi,
+ context.GetLocalAddress().GetIpAddress().ToString(),
+ *url);
+ output += "\r\n";
+ }
+
+ PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
+ response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
+ return NPT_SUCCESS;
+ }
+
+ if (URIUtils::IsURL(static_cast<const char*>(file_path)))
+ {
+ CURL url(CTextureUtils::UnwrapImageURL(static_cast<const char*>(file_path)));
+ std::string disp = "inline; filename=\"" + URIUtils::GetFileName(url) + "\"";
+ response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
+ }
+
+ // set getCaptionInfo.sec - sets subtitle uri for Samsung devices
+ const NPT_String* captionInfoHeader = request.GetHeaders().GetHeaderValue("getCaptionInfo.sec");
+ if (captionInfoHeader)
+ {
+ NPT_String *sub_uri, movie;
+ movie = "subtitle://" + md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ if (NPT_SUCCEEDED(m_FileMap.Get(movie, sub_uri)))
+ {
+ response.GetHeaders().SetHeader("CaptionInfo.sec", sub_uri->GetChars(), false);
+ }
+ }
+
+ return PLT_HttpServer::ServeFile(request,
+ context,
+ response,
+ file_path);
+}
+
+void
+CUPnPServer::DefaultSortItems(CFileItemList& items)
+{
+ CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
+ if (viewState)
+ {
+ SortDescription sorting = viewState->GetSortMethod();
+ items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
+ delete viewState;
+ }
+}
+
+NPT_Result CUPnPServer::AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri)
+{
+ /* using existing m_FileMap to store subtitle uri for movie,
+ adding subtitle:// prefix, because there is already entry for movie md5 with movie path */
+ NPT_String movie = "subtitle://" + movie_md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(movie, subtitle_uri));
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPServer::GetRequiredVideoDbDetails(const NPT_String& filter)
+{
+ int details = VideoDbDetailsRating;
+ if (filter.Find("res@resolution") >= 0 || filter.Find("res@nrAudioChannels") >= 0)
+ details |= VideoDbDetailsStream;
+ if (filter.Find("upnp:actor") >= 0)
+ details |= VideoDbDetailsCast;
+
+ return details;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPServer.h b/xbmc/network/upnp/UPnPServer.h
new file mode 100644
index 0000000..1c92e8b
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.h
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/IAnnouncer.h"
+#include "utils/logtypes.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include <Platinum/Source/Devices/MediaConnect/PltMediaConnect.h>
+
+class CFileItem;
+class CFileItemList;
+class CThumbLoader;
+class CVariant;
+class PLT_MediaObject;
+class PLT_HttpRequestContext;
+
+namespace UPNP
+{
+
+class CUPnPServer : public PLT_MediaConnect,
+ public PLT_FileMediaConnectDelegate,
+ public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CUPnPServer(const char* friendly_name, const char* uuid = NULL, int port = 0);
+ ~CUPnPServer() override;
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ // PLT_MediaServer methods
+ NPT_Result OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+ NPT_Result OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+ NPT_Result OnSearchContainer(PLT_ActionReference& action,
+ const char* container_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+
+ NPT_Result OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context) override;
+
+ // PLT_FileMediaServer methods
+ NPT_Result ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& file_path) override;
+
+ // PLT_DeviceHost methods
+ NPT_Result ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response) override;
+
+ NPT_Result SetupServices() override;
+ NPT_Result SetupIcons() override;
+ NPT_String BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path);
+
+ void AddSafeResourceUri(PLT_MediaObject* object,
+ const NPT_HttpUrl& rooturi,
+ const NPT_List<NPT_IpAddress>& ips,
+ const char* file_path,
+ const NPT_String& info)
+ {
+ PLT_MediaItemResource res;
+ for(NPT_List<NPT_IpAddress>::Iterator ip = ips.GetFirstItem(); ip; ++ip) {
+ res.m_ProtocolInfo = PLT_ProtocolInfo(info);
+ res.m_Uri = BuildSafeResourceUri(rooturi, (*ip).ToString(), file_path);
+ object->m_Resources.Add(res);
+ }
+ }
+
+ /* Samsung's devices get subtitles from header in response (for movie file), not from didl.
+ It's a way to store subtitle uri generated when building didl, to use later in http response*/
+ NPT_Result AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri);
+
+
+ private:
+ void OnScanCompleted(int type);
+ void UpdateContainer(const std::string& id);
+ void PropagateUpdates();
+
+ PLT_MediaObject* Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumbLoader,
+ const char* parent_id = NULL);
+ NPT_Result BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */);
+
+ // class methods
+ static void DefaultSortItems(CFileItemList& items);
+ static NPT_String GetParentFolder(const NPT_String& file_path)
+ {
+ int index = file_path.ReverseFind("\\");
+ if (index == -1)
+ return "";
+
+ return file_path.Left(index);
+ }
+
+ static int GetRequiredVideoDbDetails(const NPT_String& filter);
+
+ NPT_Mutex m_CacheMutex;
+
+ NPT_Mutex m_FileMutex;
+ NPT_Map<NPT_String, NPT_String> m_FileMap;
+
+ std::map<std::string, std::pair<bool, unsigned long> > m_UpdateIDs;
+ bool m_scanning;
+
+ Logger m_logger;
+
+ public:
+ // class members
+ static NPT_UInt32 m_MaxReturnedItems;
+};
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPSettings.cpp b/xbmc/network/upnp/UPnPSettings.cpp
new file mode 100644
index 0000000..30ad1fd
--- /dev/null
+++ b/xbmc/network/upnp/UPnPSettings.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "UPnPSettings.h"
+
+#include "ServiceBroker.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#define XML_UPNP "upnpserver"
+#define XML_SERVER_UUID "UUID"
+#define XML_SERVER_PORT "Port"
+#define XML_MAX_ITEMS "MaxReturnedItems"
+#define XML_RENDERER_UUID "UUIDRenderer"
+#define XML_RENDERER_PORT "PortRenderer"
+
+CUPnPSettings::CUPnPSettings() : m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPSettings"))
+{
+ Clear();
+}
+
+CUPnPSettings::~CUPnPSettings()
+{
+ Clear();
+}
+
+CUPnPSettings& CUPnPSettings::GetInstance()
+{
+ static CUPnPSettings sUPnPSettings;
+ return sUPnPSettings;
+}
+
+void CUPnPSettings::OnSettingsUnloaded()
+{
+ Clear();
+}
+
+bool CUPnPSettings::Load(const std::string &file)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ Clear();
+
+ if (!CFileUtils::Exists(file))
+ return false;
+
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(file))
+ {
+ m_logger->error("error loading {}, Line {}\n{}", file, doc.ErrorRow(), doc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pRootElement = doc.RootElement();
+ if (pRootElement == NULL || !StringUtils::EqualsNoCase(pRootElement->Value(), XML_UPNP))
+ {
+ m_logger->error("error loading {}, no <upnpserver> node", file);
+ return false;
+ }
+
+ // load settings
+ XMLUtils::GetString(pRootElement, XML_SERVER_UUID, m_serverUUID);
+ XMLUtils::GetInt(pRootElement, XML_SERVER_PORT, m_serverPort);
+ XMLUtils::GetInt(pRootElement, XML_MAX_ITEMS, m_maxReturnedItems);
+ XMLUtils::GetString(pRootElement, XML_RENDERER_UUID, m_rendererUUID);
+ XMLUtils::GetInt(pRootElement, XML_RENDERER_PORT, m_rendererPort);
+
+ return true;
+}
+
+bool CUPnPSettings::Save(const std::string &file) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ CXBMCTinyXML doc;
+ TiXmlElement xmlRootElement(XML_UPNP);
+ TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement);
+ if (pRoot == NULL)
+ return false;
+
+ XMLUtils::SetString(pRoot, XML_SERVER_UUID, m_serverUUID);
+ XMLUtils::SetInt(pRoot, XML_SERVER_PORT, m_serverPort);
+ XMLUtils::SetInt(pRoot, XML_MAX_ITEMS, m_maxReturnedItems);
+ XMLUtils::SetString(pRoot, XML_RENDERER_UUID, m_rendererUUID);
+ XMLUtils::SetInt(pRoot, XML_RENDERER_PORT, m_rendererPort);
+
+ // save the file
+ return doc.SaveFile(file);
+}
+
+void CUPnPSettings::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ m_serverUUID.clear();
+ m_serverPort = 0;
+ m_maxReturnedItems = 0;
+ m_rendererUUID.clear();
+ m_rendererPort = 0;
+}
diff --git a/xbmc/network/upnp/UPnPSettings.h b/xbmc/network/upnp/UPnPSettings.h
new file mode 100644
index 0000000..7469b06
--- /dev/null
+++ b/xbmc/network/upnp/UPnPSettings.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/logtypes.h"
+
+#include <string>
+
+class CUPnPSettings : public ISettingsHandler
+{
+public:
+ static CUPnPSettings& GetInstance();
+
+ void OnSettingsUnloaded() override;
+
+ bool Load(const std::string &file);
+ bool Save(const std::string &file) const;
+ void Clear();
+
+ const std::string& GetServerUUID() const { return m_serverUUID; }
+ int GetServerPort() const { return m_serverPort; }
+ int GetMaximumReturnedItems() const { return m_maxReturnedItems; }
+ const std::string& GetRendererUUID() const { return m_rendererUUID; }
+ int GetRendererPort() const { return m_rendererPort; }
+
+ void SetServerUUID(const std::string &uuid) { m_serverUUID = uuid; }
+ void SetServerPort(int port) { m_serverPort = port; }
+ void SetMaximumReturnedItems(int maximumReturnedItems) { m_maxReturnedItems = maximumReturnedItems; }
+ void SetRendererUUID(const std::string &uuid) { m_rendererUUID = uuid; }
+ void SetRendererPort(int port) { m_rendererPort = port; }
+
+protected:
+ CUPnPSettings();
+ CUPnPSettings(const CUPnPSettings&) = delete;
+ CUPnPSettings& operator=(CUPnPSettings const&) = delete;
+ ~CUPnPSettings() override;
+
+private:
+ std::string m_serverUUID;
+ int m_serverPort;
+ int m_maxReturnedItems;
+ std::string m_rendererUUID;
+ int m_rendererPort;
+
+ mutable CCriticalSection m_critical;
+
+ Logger m_logger;
+};
diff --git a/xbmc/network/websocket/CMakeLists.txt b/xbmc/network/websocket/CMakeLists.txt
new file mode 100644
index 0000000..306cd6c
--- /dev/null
+++ b/xbmc/network/websocket/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES WebSocket.cpp
+ WebSocketManager.cpp
+ WebSocketV13.cpp
+ WebSocketV8.cpp)
+
+set(HEADERS WebSocket.h
+ WebSocketManager.h
+ WebSocketV13.h
+ WebSocketV8.h)
+
+core_add_library(network_websockets)
diff --git a/xbmc/network/websocket/WebSocket.cpp b/xbmc/network/websocket/WebSocket.cpp
new file mode 100644
index 0000000..bbf4a01
--- /dev/null
+++ b/xbmc/network/websocket/WebSocket.cpp
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WebSocket.h"
+
+#include "utils/EndianSwap.h"
+#include "utils/HttpParser.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+#include <string>
+
+#define MASK_FIN 0x80
+#define MASK_RSV1 0x40
+#define MASK_RSV2 0x20
+#define MASK_RSV3 0x10
+#define MASK_RSV (MASK_RSV1 | MASK_RSV2 | MASK_RSV3)
+#define MASK_OPCODE 0x0F
+#define MASK_MASK 0x80
+#define MASK_LENGTH 0x7F
+
+#define CONTROL_FRAME 0x08
+
+#define LENGTH_MIN 0x2
+
+CWebSocketFrame::CWebSocketFrame(const char* data, uint64_t length)
+{
+ reset();
+
+ if (data == NULL || length < LENGTH_MIN)
+ return;
+
+ m_free = false;
+ m_data = data;
+ m_lengthFrame = length;
+
+ // Get the FIN flag
+ m_final = ((m_data[0] & MASK_FIN) == MASK_FIN);
+ // Get the RSV1 - RSV3 flags
+ m_extension |= m_data[0] & MASK_RSV1;
+ m_extension |= (m_data[0] & MASK_RSV2) << 1;
+ m_extension |= (m_data[0] & MASK_RSV3) << 2;
+ // Get the opcode
+ m_opcode = (WebSocketFrameOpcode)(m_data[0] & MASK_OPCODE);
+ if (m_opcode >= WebSocketUnknownFrame)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Frame with invalid opcode {:2X} received", m_opcode);
+ reset();
+ return;
+ }
+ if ((m_opcode & CONTROL_FRAME) == CONTROL_FRAME && !m_final)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Fragmented control frame (opcode {:2X}) received", m_opcode);
+ reset();
+ return;
+ }
+
+ // Get the MASK flag
+ m_masked = ((m_data[1] & MASK_MASK) == MASK_MASK);
+
+ // Get the payload length
+ m_length = (uint64_t)(m_data[1] & MASK_LENGTH);
+ if ((m_length <= 125 && m_lengthFrame < m_length + LENGTH_MIN) ||
+ (m_length == 126 && m_lengthFrame < LENGTH_MIN + 2) ||
+ (m_length == 127 && m_lengthFrame < LENGTH_MIN + 8))
+ {
+ CLog::Log(LOGINFO, "WebSocket: Frame with invalid length received");
+ reset();
+ return;
+ }
+
+ if (IsControlFrame() && (m_length > 125 || !m_final))
+ {
+ CLog::Log(LOGWARNING, "WebSocket: Invalid control frame received");
+ reset();
+ return;
+ }
+
+ int offset = 0;
+ if (m_length == 126)
+ {
+ m_length = (uint64_t)Endian_SwapBE16(*(const uint16_t *)(m_data + 2));
+ offset = 2;
+ }
+ else if (m_length == 127)
+ {
+ m_length = Endian_SwapBE64(*(const uint64_t *)(m_data + 2));
+ offset = 8;
+ }
+
+ if (m_lengthFrame < LENGTH_MIN + offset + m_length)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Frame with invalid length received");
+ reset();
+ return;
+ }
+
+ // Get the mask
+ if (m_masked)
+ {
+ m_mask = *(const uint32_t *)(m_data + LENGTH_MIN + offset);
+ offset += 4;
+ }
+
+ if (m_lengthFrame != LENGTH_MIN + offset + m_length)
+ m_lengthFrame = LENGTH_MIN + offset + m_length;
+
+ // Get application data
+ if (m_length > 0)
+ m_applicationData = const_cast<char *>(m_data + LENGTH_MIN + offset);
+ else
+ m_applicationData = NULL;
+
+ // Unmask the application data if necessary
+ if (m_masked)
+ {
+ for (uint64_t index = 0; index < m_length; index++)
+ m_applicationData[index] = m_applicationData[index] ^ ((char *)(&m_mask))[index % 4];
+ }
+
+ m_valid = true;
+}
+
+CWebSocketFrame::CWebSocketFrame(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */,
+ bool final /* = true */, bool masked /* = false */, int32_t mask /* = 0 */, int8_t extension /* = 0 */)
+{
+ reset();
+
+ if (opcode >= WebSocketUnknownFrame)
+ return;
+
+ m_free = true;
+ m_opcode = opcode;
+
+ m_length = length;
+
+ m_masked = masked;
+ m_mask = mask;
+ m_final = final;
+ m_extension = extension;
+
+ std::string buffer;
+ char dataByte = 0;
+
+ // Set the FIN flag
+ if (m_final)
+ dataByte |= MASK_FIN;
+
+ // Set RSV1 - RSV3 flags
+ if (m_extension != 0)
+ dataByte |= (m_extension << 4) & MASK_RSV;
+
+ // Set opcode flag
+ dataByte |= opcode & MASK_OPCODE;
+
+ buffer.push_back(dataByte);
+ dataByte = 0;
+
+ // Set MASK flag
+ if (m_masked)
+ dataByte |= MASK_MASK;
+
+ // Set payload length
+ if (m_length < 126)
+ {
+ dataByte |= m_length & MASK_LENGTH;
+ buffer.push_back(dataByte);
+ }
+ else if (m_length <= 65535)
+ {
+ dataByte |= 126 & MASK_LENGTH;
+ buffer.push_back(dataByte);
+
+ uint16_t dataLength = Endian_SwapBE16((uint16_t)m_length);
+ buffer.append((const char*)&dataLength, 2);
+ }
+ else
+ {
+ dataByte |= 127 & MASK_LENGTH;
+ buffer.push_back(dataByte);
+
+ uint64_t dataLength = Endian_SwapBE64(m_length);
+ buffer.append((const char*)&dataLength, 8);
+ }
+
+ uint64_t applicationDataOffset = 0;
+ if (data)
+ {
+ // Set masking key
+ if (m_masked)
+ {
+ buffer.append((char *)&m_mask, sizeof(m_mask));
+ applicationDataOffset = buffer.size();
+
+ for (uint64_t index = 0; index < m_length; index++)
+ buffer.push_back(data[index] ^ ((char *)(&m_mask))[index % 4]);
+ }
+ else
+ {
+ applicationDataOffset = buffer.size();
+ buffer.append(data, (unsigned int)length);
+ }
+ }
+
+ // Get the whole data
+ m_lengthFrame = buffer.size();
+ m_data = new char[(uint32_t)m_lengthFrame];
+ memcpy(const_cast<char *>(m_data), buffer.c_str(), (uint32_t)m_lengthFrame);
+
+ if (data)
+ {
+ m_applicationData = const_cast<char *>(m_data);
+ m_applicationData += applicationDataOffset;
+ }
+
+ m_valid = true;
+}
+
+CWebSocketFrame::~CWebSocketFrame()
+{
+ if (!m_valid)
+ return;
+
+ if (m_free && m_data != NULL)
+ {
+ delete[] m_data;
+ m_data = NULL;
+ }
+}
+
+void CWebSocketFrame::reset()
+{
+ m_free = false;
+ m_data = NULL;
+ m_lengthFrame = 0;
+ m_length = 0;
+ m_valid = false;
+ m_final = false;
+ m_extension = 0;
+ m_opcode = WebSocketUnknownFrame;
+ m_masked = false;
+ m_mask = 0;
+ m_applicationData = NULL;
+}
+
+CWebSocketMessage::CWebSocketMessage()
+{
+ Clear();
+}
+
+CWebSocketMessage::~CWebSocketMessage()
+{
+ for (unsigned int index = 0; index < m_frames.size(); index++)
+ delete m_frames[index];
+
+ m_frames.clear();
+}
+
+bool CWebSocketMessage::AddFrame(const CWebSocketFrame *frame)
+{
+ if (!frame->IsValid() || m_complete)
+ return false;
+
+ if (frame->IsFinal())
+ m_complete = true;
+ else
+ m_fragmented = true;
+
+ m_frames.push_back(frame);
+
+ return true;
+}
+
+void CWebSocketMessage::Clear()
+{
+ m_fragmented = false;
+ m_complete = false;
+
+ m_frames.clear();
+}
+
+const CWebSocketMessage* CWebSocket::Handle(const char* &buffer, size_t &length, bool &send)
+{
+ send = false;
+
+ while (length > 0)
+ {
+ switch (m_state)
+ {
+ case WebSocketStateConnected:
+ {
+ CWebSocketFrame *frame = GetFrame(buffer, length);
+ if (!frame->IsValid())
+ {
+ CLog::Log(LOGINFO, "WebSocket: Invalid frame received");
+ delete frame;
+ return NULL;
+ }
+
+ // adjust the length and the buffer values
+ length -= (size_t)frame->GetFrameLength();
+ buffer += frame->GetFrameLength();
+
+ if (frame->IsControlFrame())
+ {
+ if (!frame->IsFinal())
+ {
+ delete frame;
+ return NULL;
+ }
+
+ CWebSocketMessage *msg = NULL;
+ switch (frame->GetOpcode())
+ {
+ case WebSocketPing:
+ msg = GetMessage();
+ if (msg != NULL)
+ msg->AddFrame(Pong(frame->GetApplicationData()));
+ break;
+
+ case WebSocketConnectionClose:
+ CLog::Log(LOGINFO, "WebSocket: connection closed by client");
+
+ msg = GetMessage();
+ if (msg != NULL)
+ msg->AddFrame(Close());
+
+ m_state = WebSocketStateClosed;
+ break;
+
+ case WebSocketContinuationFrame:
+ case WebSocketTextFrame:
+ case WebSocketBinaryFrame:
+ case WebSocketPong:
+ case WebSocketUnknownFrame:
+ default:
+ break;
+ }
+
+ delete frame;
+
+ if (msg != NULL)
+ send = true;
+
+ return msg;
+ }
+
+ if (m_message == NULL && (m_message = GetMessage()) == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Could not allocate a new websocket message");
+ delete frame;
+ return NULL;
+ }
+
+ m_message->AddFrame(frame);
+ if (!m_message->IsComplete())
+ {
+ if (length > 0)
+ continue;
+ else
+ return NULL;
+ }
+
+ CWebSocketMessage *msg = m_message;
+ m_message = NULL;
+ return msg;
+ }
+
+ case WebSocketStateClosing:
+ {
+ CWebSocketFrame *frame = GetFrame(buffer, length);
+
+ if (frame->IsValid())
+ {
+ // adjust the length and the buffer values
+ length -= (size_t)frame->GetFrameLength();
+ buffer += frame->GetFrameLength();
+ }
+
+ if (!frame->IsValid() || frame->GetOpcode() == WebSocketConnectionClose)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Invalid or unexpected frame received (only closing handshake expected)");
+ delete frame;
+ return NULL;
+ }
+
+ m_state = WebSocketStateClosed;
+ return NULL;
+ }
+
+ case WebSocketStateNotConnected:
+ case WebSocketStateClosed:
+ case WebSocketStateHandshaking:
+ default:
+ CLog::Log(LOGINFO, "WebSocket: No frame expected in the current state");
+ return NULL;
+ }
+ }
+
+ return NULL;
+}
+
+const CWebSocketMessage* CWebSocket::Send(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */)
+{
+ CWebSocketFrame *frame = GetFrame(opcode, data, length);
+ if (frame == NULL || !frame->IsValid())
+ {
+ CLog::Log(LOGINFO, "WebSocket: Trying to send an invalid frame");
+ return NULL;
+ }
+
+ CWebSocketMessage *msg = GetMessage();
+ if (msg == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Could not allocate a message");
+ return NULL;
+ }
+
+ msg->AddFrame(frame);
+ if (msg->IsComplete())
+ return msg;
+
+ return NULL;
+}
diff --git a/xbmc/network/websocket/WebSocket.h b/xbmc/network/websocket/WebSocket.h
new file mode 100644
index 0000000..8901ede
--- /dev/null
+++ b/xbmc/network/websocket/WebSocket.h
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+enum WebSocketFrameOpcode
+{
+ WebSocketContinuationFrame = 0x00,
+ WebSocketTextFrame = 0x01,
+ WebSocketBinaryFrame = 0x02,
+ //0x3 - 0x7 are reserved for non-control frames
+ WebSocketConnectionClose = 0x08,
+ WebSocketPing = 0x09,
+ WebSocketPong = 0x0A,
+ //0xB - 0xF are reserved for control frames
+ WebSocketUnknownFrame = 0x10
+};
+
+enum WebSocketState
+{
+ WebSocketStateNotConnected = 0,
+ WebSocketStateHandshaking = 1,
+ WebSocketStateConnected = 2,
+ WebSocketStateClosing = 3,
+ WebSocketStateClosed = 4
+};
+
+enum WebSocketCloseReason
+{
+ WebSocketCloseNormal = 1000,
+ WebSocketCloseLeaving = 1001,
+ WebSocketCloseProtocolError = 1002,
+ WebSocketCloseInvalidData = 1003,
+ WebSocketCloseFrameTooLarge = 1004,
+ // Reserved status code = 1005,
+ // Reserved status code = 1006,
+ WebSocketCloseInvalidUtf8 = 1007
+};
+
+class CWebSocketFrame
+{
+public:
+ CWebSocketFrame(const char* data, uint64_t length);
+ CWebSocketFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0);
+ virtual ~CWebSocketFrame();
+
+ virtual bool IsValid() const { return m_valid; }
+ virtual uint64_t GetFrameLength() const { return m_lengthFrame; }
+ virtual bool IsFinal() const { return m_final; }
+ virtual int8_t GetExtension() const { return m_extension; }
+ virtual WebSocketFrameOpcode GetOpcode() const { return m_opcode; }
+ virtual bool IsControlFrame() const { return (m_valid && (m_opcode & 0x8) == 0x8); }
+ virtual bool IsMasked() const { return m_masked; }
+ virtual uint64_t GetLength() const { return m_length; }
+ virtual int32_t GetMask() const { return m_mask; }
+ virtual const char* GetFrameData() const { return m_data; }
+ virtual const char* GetApplicationData() const { return m_applicationData; }
+
+protected:
+ bool m_free;
+ const char *m_data;
+ uint64_t m_lengthFrame;
+ uint64_t m_length;
+ bool m_valid;
+ bool m_final;
+ int8_t m_extension;
+ WebSocketFrameOpcode m_opcode;
+ bool m_masked;
+ int32_t m_mask;
+ char *m_applicationData;
+
+private:
+ void reset();
+ CWebSocketFrame(const CWebSocketFrame&) = delete;
+ CWebSocketFrame& operator=(const CWebSocketFrame&) = delete;
+};
+
+class CWebSocketMessage
+{
+public:
+ CWebSocketMessage();
+ virtual ~CWebSocketMessage();
+
+ virtual bool IsFragmented() const { return m_fragmented; }
+ virtual bool IsComplete() const { return m_complete; }
+
+ virtual bool AddFrame(const CWebSocketFrame* frame);
+ virtual const std::vector<const CWebSocketFrame *>& GetFrames() const { return m_frames; }
+
+ virtual void Clear();
+
+protected:
+ std::vector<const CWebSocketFrame *> m_frames;
+ bool m_fragmented;
+ bool m_complete;
+};
+
+class CWebSocket
+{
+public:
+ CWebSocket() { m_state = WebSocketStateNotConnected; m_message = NULL; }
+ virtual ~CWebSocket()
+ {
+ if (m_message)
+ delete m_message;
+ }
+
+ int GetVersion() { return m_version; }
+ WebSocketState GetState() { return m_state; }
+
+ virtual bool Handshake(const char* data, size_t length, std::string &response) = 0;
+ virtual const CWebSocketMessage* Handle(const char* &buffer, size_t &length, bool &send);
+ virtual const CWebSocketMessage* Send(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0);
+ virtual const CWebSocketFrame* Ping(const char* data = NULL) const = 0;
+ virtual const CWebSocketFrame* Pong(const char* data = NULL) const = 0;
+ virtual const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") = 0;
+ virtual void Fail() = 0;
+
+protected:
+ int m_version;
+ WebSocketState m_state;
+ CWebSocketMessage *m_message;
+
+ virtual CWebSocketFrame* GetFrame(const char* data, uint64_t length) = 0;
+ virtual CWebSocketFrame* GetFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0) = 0;
+ virtual CWebSocketMessage* GetMessage() = 0;
+};
diff --git a/xbmc/network/websocket/WebSocketManager.cpp b/xbmc/network/websocket/WebSocketManager.cpp
new file mode 100644
index 0000000..6fef69a
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketManager.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WebSocketManager.h"
+
+#include "WebSocket.h"
+#include "WebSocketV13.h"
+#include "WebSocketV8.h"
+#include "utils/HttpParser.h"
+#include "utils/HttpResponse.h"
+#include "utils/log.h"
+
+#include <string>
+
+#define WS_HTTP_METHOD "GET"
+#define WS_HTTP_TAG "HTTP/"
+#define WS_SUPPORTED_VERSIONS "8, 13"
+
+#define WS_HEADER_VERSION "Sec-WebSocket-Version"
+#define WS_HEADER_VERSION_LC "sec-websocket-version" // "Sec-WebSocket-Version"
+
+CWebSocket* CWebSocketManager::Handle(const char* data, unsigned int length, std::string &response)
+{
+ if (data == NULL || length <= 0)
+ return NULL;
+
+ HttpParser header;
+ HttpParser::status_t status = header.addBytes(data, length);
+ switch (status)
+ {
+ case HttpParser::Error:
+ case HttpParser::Incomplete:
+ response.clear();
+ return NULL;
+
+ case HttpParser::Done:
+ default:
+ break;
+ }
+
+ // There must be a "Sec-WebSocket-Version" header
+ const char* value = header.getValue(WS_HEADER_VERSION_LC);
+ if (value == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: missing Sec-WebSocket-Version");
+ CHttpResponse httpResponse(HTTP::Get, HTTP::BadRequest, HTTP::Version1_1);
+ response = httpResponse.Create();
+
+ return NULL;
+ }
+
+ CWebSocket *websocket = NULL;
+ if (strncmp(value, "8", 1) == 0)
+ websocket = new CWebSocketV8();
+ else if (strncmp(value, "13", 2) == 0)
+ websocket = new CWebSocketV13();
+
+ if (websocket == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Unsupported Sec-WebSocket-Version {}", value);
+ CHttpResponse httpResponse(HTTP::Get, HTTP::UpgradeRequired, HTTP::Version1_1);
+ httpResponse.AddHeader(WS_HEADER_VERSION, WS_SUPPORTED_VERSIONS);
+ response = httpResponse.Create();
+
+ return NULL;
+ }
+
+ if (websocket->Handshake(data, length, response))
+ return websocket;
+
+ return NULL;
+}
diff --git a/xbmc/network/websocket/WebSocketManager.h b/xbmc/network/websocket/WebSocketManager.h
new file mode 100644
index 0000000..7cbe8e9
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketManager.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+class CWebSocket;
+
+class CWebSocketManager
+{
+public:
+ static CWebSocket* Handle(const char* data, unsigned int length, std::string &response);
+};
diff --git a/xbmc/network/websocket/WebSocketV13.cpp b/xbmc/network/websocket/WebSocketV13.cpp
new file mode 100644
index 0000000..e0ef8f0
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV13.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WebSocketV13.h"
+
+#include "WebSocket.h"
+#include "utils/HttpParser.h"
+#include "utils/HttpResponse.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <sstream>
+#include <string>
+
+#define WS_HTTP_METHOD "GET"
+#define WS_HTTP_TAG "HTTP/"
+
+#define WS_HEADER_UPGRADE "Upgrade"
+#define WS_HEADER_UPGRADE_LC "upgrade"
+#define WS_HEADER_CONNECTION "Connection"
+#define WS_HEADER_CONNECTION_LC "connection"
+
+#define WS_HEADER_KEY_LC "sec-websocket-key" // "Sec-WebSocket-Key"
+#define WS_HEADER_ACCEPT "Sec-WebSocket-Accept"
+#define WS_HEADER_PROTOCOL "Sec-WebSocket-Protocol"
+#define WS_HEADER_PROTOCOL_LC "sec-websocket-protocol" // "Sec-WebSocket-Protocol"
+
+#define WS_PROTOCOL_JSONRPC "jsonrpc.xbmc.org"
+#define WS_HEADER_UPGRADE_VALUE "websocket"
+
+bool CWebSocketV13::Handshake(const char* data, size_t length, std::string &response)
+{
+ std::string strHeader(data, length);
+ const char *value;
+ HttpParser header;
+ if (header.addBytes(data, length) != HttpParser::Done)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: incomplete handshake received");
+ return false;
+ }
+
+ // The request must be GET
+ value = header.getMethod();
+ if (value == NULL ||
+ StringUtils::CompareNoCase(value, WS_HTTP_METHOD, strlen(WS_HTTP_METHOD)) != 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid HTTP method received (GET expected)");
+ return false;
+ }
+
+ // The request must be HTTP/1.1 or higher
+ size_t pos;
+ if ((pos = strHeader.find(WS_HTTP_TAG)) == std::string::npos)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid handshake received");
+ return false;
+ }
+
+ pos += strlen(WS_HTTP_TAG);
+ std::istringstream converter(strHeader.substr(pos, strHeader.find_first_of(" \r\n\t", pos) - pos));
+ float fVersion;
+ converter >> fVersion;
+
+ if (fVersion < 1.1f)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid HTTP version {:f} (1.1 or higher expected)",
+ fVersion);
+ return false;
+ }
+
+ std::string websocketKey, websocketProtocol;
+ // There must be a "Host" header
+ value = header.getValue("host");
+ if (value == NULL || strlen(value) == 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: \"Host\" header missing");
+ return true;
+ }
+
+ // There must be a "Upgrade" header with the value "websocket"
+ value = header.getValue(WS_HEADER_UPGRADE_LC);
+ if (value == NULL || StringUtils::CompareNoCase(value, WS_HEADER_UPGRADE_VALUE,
+ strlen(WS_HEADER_UPGRADE_VALUE)) != 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"{}\" received", WS_HEADER_UPGRADE);
+ return true;
+ }
+
+ // There must be a "Connection" header with the value "Upgrade"
+ value = header.getValue(WS_HEADER_CONNECTION_LC);
+ std::vector<std::string> elements;
+ if (value != nullptr)
+ elements = StringUtils::Split(value, ",");
+ if (elements.empty() || !std::any_of(elements.begin(), elements.end(), [](std::string& elem) { return StringUtils::EqualsNoCase(StringUtils::Trim(elem), WS_HEADER_UPGRADE); }))
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"{}\" received", WS_HEADER_CONNECTION_LC);
+ return true;
+ }
+
+ // There must be a base64 encoded 16 byte (=> 24 byte as base62) "Sec-WebSocket-Key" header
+ value = header.getValue(WS_HEADER_KEY_LC);
+ if (value == NULL || (websocketKey = value).size() != 24)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"Sec-WebSocket-Key\" received");
+ return true;
+ }
+
+ // There might be a "Sec-WebSocket-Protocol" header
+ value = header.getValue(WS_HEADER_PROTOCOL_LC);
+ if (value && strlen(value) > 0)
+ {
+ std::vector<std::string> protocols = StringUtils::Split(value, ",");
+ for (auto& protocol : protocols)
+ {
+ StringUtils::Trim(protocol);
+ if (protocol == WS_PROTOCOL_JSONRPC)
+ {
+ websocketProtocol = WS_PROTOCOL_JSONRPC;
+ break;
+ }
+ }
+ }
+
+ CHttpResponse httpResponse(HTTP::Get, HTTP::SwitchingProtocols, HTTP::Version1_1);
+ httpResponse.AddHeader(WS_HEADER_UPGRADE, WS_HEADER_UPGRADE_VALUE);
+ httpResponse.AddHeader(WS_HEADER_CONNECTION, WS_HEADER_UPGRADE);
+ std::string responseKey = calculateKey(websocketKey);
+ httpResponse.AddHeader(WS_HEADER_ACCEPT, responseKey);
+ if (!websocketProtocol.empty())
+ httpResponse.AddHeader(WS_HEADER_PROTOCOL, websocketProtocol);
+
+ response = httpResponse.Create();
+
+ m_state = WebSocketStateConnected;
+
+ return true;
+}
+
+const CWebSocketFrame* CWebSocketV13::Close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */)
+{
+ if (m_state == WebSocketStateNotConnected || m_state == WebSocketStateHandshaking || m_state == WebSocketStateClosed)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: Cannot send a closing handshake if no connection has been established");
+ return NULL;
+ }
+
+ return close(reason, message);
+}
diff --git a/xbmc/network/websocket/WebSocketV13.h b/xbmc/network/websocket/WebSocketV13.h
new file mode 100644
index 0000000..80c5a73
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV13.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "WebSocketV8.h"
+
+#include <string>
+
+class CWebSocketV13 : public CWebSocketV8
+{
+public:
+ CWebSocketV13() { m_version = 13; }
+
+ bool Handshake(const char* data, size_t length, std::string &response) override;
+ const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") override;
+};
diff --git a/xbmc/network/websocket/WebSocketV8.cpp b/xbmc/network/websocket/WebSocketV8.cpp
new file mode 100644
index 0000000..e0a187b
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV8.cpp
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WebSocketV8.h"
+
+#include "WebSocket.h"
+#include "utils/Base64.h"
+#include "utils/Digest.h"
+#include "utils/EndianSwap.h"
+#include "utils/HttpParser.h"
+#include "utils/HttpResponse.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+#include <string>
+
+using KODI::UTILITY::CDigest;
+
+#define WS_HTTP_METHOD "GET"
+#define WS_HTTP_TAG "HTTP/"
+
+#define WS_HEADER_UPGRADE "Upgrade"
+#define WS_HEADER_CONNECTION "Connection"
+
+#define WS_HEADER_KEY_LC "sec-websocket-key" // "Sec-WebSocket-Key"
+#define WS_HEADER_ACCEPT "Sec-WebSocket-Accept"
+#define WS_HEADER_PROTOCOL "Sec-WebSocket-Protocol"
+#define WS_HEADER_PROTOCOL_LC "sec-websocket-protocol" // "Sec-WebSocket-Protocol"
+
+#define WS_PROTOCOL_JSONRPC "jsonrpc.xbmc.org"
+#define WS_HEADER_UPGRADE_VALUE "websocket"
+#define WS_KEY_MAGICSTRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+bool CWebSocketV8::Handshake(const char* data, size_t length, std::string &response)
+{
+ std::string strHeader(data, length);
+ const char *value;
+ HttpParser header;
+ if (header.addBytes(data, length) != HttpParser::Done)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: incomplete handshake received");
+ return false;
+ }
+
+ // The request must be GET
+ value = header.getMethod();
+ if (value == NULL ||
+ StringUtils::CompareNoCase(value, WS_HTTP_METHOD, strlen(WS_HTTP_METHOD)) != 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid HTTP method received (GET expected)");
+ return false;
+ }
+
+ // The request must be HTTP/1.1 or higher
+ size_t pos;
+ if ((pos = strHeader.find(WS_HTTP_TAG)) == std::string::npos)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid handshake received");
+ return false;
+ }
+
+ pos += strlen(WS_HTTP_TAG);
+ std::istringstream converter(strHeader.substr(pos, strHeader.find_first_of(" \r\n\t", pos) - pos));
+ float fVersion;
+ converter >> fVersion;
+
+ if (fVersion < 1.1f)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid HTTP version {:f} (1.1 or higher expected)",
+ fVersion);
+ return false;
+ }
+
+ std::string websocketKey, websocketProtocol;
+ // There must be a "Host" header
+ value = header.getValue("host");
+ if (value == NULL || strlen(value) == 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: \"Host\" header missing");
+ return true;
+ }
+
+ // There must be a base64 encoded 16 byte (=> 24 byte as base64) "Sec-WebSocket-Key" header
+ value = header.getValue(WS_HEADER_KEY_LC);
+ if (value == NULL || (websocketKey = value).size() != 24)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid \"Sec-WebSocket-Key\" received");
+ return true;
+ }
+
+ // There might be a "Sec-WebSocket-Protocol" header
+ value = header.getValue(WS_HEADER_PROTOCOL_LC);
+ if (value && strlen(value) > 0)
+ {
+ std::vector<std::string> protocols = StringUtils::Split(value, ",");
+ for (auto& protocol : protocols)
+ {
+ StringUtils::Trim(protocol);
+ if (protocol == WS_PROTOCOL_JSONRPC)
+ {
+ websocketProtocol = WS_PROTOCOL_JSONRPC;
+ break;
+ }
+ }
+ }
+
+ CHttpResponse httpResponse(HTTP::Get, HTTP::SwitchingProtocols, HTTP::Version1_1);
+ httpResponse.AddHeader(WS_HEADER_UPGRADE, WS_HEADER_UPGRADE_VALUE);
+ httpResponse.AddHeader(WS_HEADER_CONNECTION, WS_HEADER_UPGRADE);
+ httpResponse.AddHeader(WS_HEADER_ACCEPT, calculateKey(websocketKey));
+ if (!websocketProtocol.empty())
+ httpResponse.AddHeader(WS_HEADER_PROTOCOL, websocketProtocol);
+
+ response = httpResponse.Create();
+
+ m_state = WebSocketStateConnected;
+
+ return true;
+}
+
+const CWebSocketFrame* CWebSocketV8::Close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */)
+{
+ if (m_state == WebSocketStateNotConnected || m_state == WebSocketStateHandshaking || m_state == WebSocketStateClosed)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: Cannot send a closing handshake if no connection has been established");
+ return NULL;
+ }
+
+ return close(reason, message);
+}
+
+void CWebSocketV8::Fail()
+{
+ m_state = WebSocketStateClosed;
+}
+
+CWebSocketFrame* CWebSocketV8::GetFrame(const char* data, uint64_t length)
+{
+ return new CWebSocketFrame(data, length);
+}
+
+CWebSocketFrame* CWebSocketV8::GetFrame(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */,
+ bool final /* = true */, bool masked /* = false */, int32_t mask /* = 0 */, int8_t extension /* = 0 */)
+{
+ return new CWebSocketFrame(opcode, data, length, final, masked, mask, extension);
+}
+
+CWebSocketMessage* CWebSocketV8::GetMessage()
+{
+ return new CWebSocketMessage();
+}
+
+const CWebSocketFrame* CWebSocketV8::close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */)
+{
+ size_t length = 2 + message.size();
+
+ char* data = new char[length + 1];
+ memset(data, 0, length + 1);
+ uint16_t iReason = Endian_SwapBE16((uint16_t)reason);
+ memcpy(data, &iReason, 2);
+ message.copy(data + 2, message.size());
+
+ if (m_state == WebSocketStateConnected)
+ m_state = WebSocketStateClosing;
+ else
+ m_state = WebSocketStateClosed;
+
+ CWebSocketFrame* frame = new CWebSocketFrame(WebSocketConnectionClose, data, length);
+ delete[] data;
+
+ return frame;
+}
+
+std::string CWebSocketV8::calculateKey(const std::string &key)
+{
+ std::string acceptKey = key;
+ acceptKey.append(WS_KEY_MAGICSTRING);
+
+ CDigest digest{CDigest::Type::SHA1};
+ digest.Update(acceptKey);
+
+ return Base64::Encode(digest.FinalizeRaw());
+}
diff --git a/xbmc/network/websocket/WebSocketV8.h b/xbmc/network/websocket/WebSocketV8.h
new file mode 100644
index 0000000..d86688c
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV8.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "WebSocket.h"
+
+#include <string>
+
+class CWebSocketV8 : public CWebSocket
+{
+public:
+ CWebSocketV8() { m_version = 8; }
+
+ bool Handshake(const char* data, size_t length, std::string &response) override;
+ const CWebSocketFrame* Ping(const char* data = NULL) const override { return new CWebSocketFrame(WebSocketPing, data); }
+ const CWebSocketFrame* Pong(const char* data = NULL) const override { return new CWebSocketFrame(WebSocketPong, data); }
+ const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") override;
+ void Fail() override;
+
+protected:
+ CWebSocketFrame* GetFrame(const char* data, uint64_t length) override;
+ CWebSocketFrame* GetFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0) override;
+ CWebSocketMessage* GetMessage() override;
+ virtual const CWebSocketFrame* close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "");
+
+ std::string calculateKey(const std::string &key);
+};