diff options
Diffstat (limited to 'xbmc/network')
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(<ime)); //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, "<", "<"); + StringUtils::Replace(output, ">", ">"); + StringUtils::Replace(output, " ", " "); + 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 Binary files differnew file mode 100644 index 0000000..f792601 --- /dev/null +++ b/xbmc/network/test/data/webserver/test.png 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); +}; |