summaryrefslogtreecommitdiffstats
path: root/xbmc/network/AirPlayServer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/network/AirPlayServer.cpp')
-rw-r--r--xbmc/network/AirPlayServer.cpp1225
1 files changed, 1225 insertions, 0 deletions
diff --git a/xbmc/network/AirPlayServer.cpp b/xbmc/network/AirPlayServer.cpp
new file mode 100644
index 0000000..74251d3
--- /dev/null
+++ b/xbmc/network/AirPlayServer.cpp
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * Many concepts and protocol specification in this code are taken
+ * from Airplayer. https://github.com/PascalW/Airplayer
+ */
+
+#include "AirPlayServer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#ifdef HAS_ZEROCONF
+#include "network/Zeroconf.h"
+#endif // HAS_ZEROCONF
+
+#include <inttypes.h>
+
+#include <plist/plist.h>
+
+using KODI::UTILITY::CDigest;
+using namespace std::chrono_literals;
+
+#ifdef TARGET_WINDOWS
+#define close closesocket
+#endif
+
+#define RECEIVEBUFFER 1024
+
+#define AIRPLAY_STATUS_OK 200
+#define AIRPLAY_STATUS_SWITCHING_PROTOCOLS 101
+#define AIRPLAY_STATUS_NEED_AUTH 401
+#define AIRPLAY_STATUS_NOT_FOUND 404
+#define AIRPLAY_STATUS_METHOD_NOT_ALLOWED 405
+#define AIRPLAY_STATUS_PRECONDITION_FAILED 412
+#define AIRPLAY_STATUS_NOT_IMPLEMENTED 501
+#define AIRPLAY_STATUS_NO_RESPONSE_NEEDED 1000
+
+CCriticalSection CAirPlayServer::ServerInstanceLock;
+CAirPlayServer *CAirPlayServer::ServerInstance = NULL;
+int CAirPlayServer::m_isPlaying = 0;
+
+#define EVENT_NONE -1
+#define EVENT_PLAYING 0
+#define EVENT_PAUSED 1
+#define EVENT_LOADING 2
+#define EVENT_STOPPED 3
+const char *eventStrings[] = {"playing", "paused", "loading", "stopped"};
+
+#define PLAYBACK_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>duration</key>\r\n" \
+ "<real>{:f}</real>\r\n" \
+ "<key>loadedTimeRanges</key>\r\n" \
+ "<array>\r\n" \
+ "\t\t<dict>\r\n" \
+ "\t\t\t<key>duration</key>\r\n" \
+ "\t\t\t<real>{:f}</real>\r\n" \
+ "\t\t\t<key>start</key>\r\n" \
+ "\t\t\t<real>0.0</real>\r\n" \
+ "\t\t</dict>\r\n" \
+ "</array>\r\n" \
+ "<key>playbackBufferEmpty</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>playbackBufferFull</key>\r\n" \
+ "<false/>\r\n" \
+ "<key>playbackLikelyToKeepUp</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>position</key>\r\n" \
+ "<real>{:f}</real>\r\n" \
+ "<key>rate</key>\r\n" \
+ "<real>{:d}</real>\r\n" \
+ "<key>readyToPlay</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>seekableTimeRanges</key>\r\n" \
+ "<array>\r\n" \
+ "\t\t<dict>\r\n" \
+ "\t\t\t<key>duration</key>\r\n" \
+ "\t\t\t<real>{:f}</real>\r\n" \
+ "\t\t\t<key>start</key>\r\n" \
+ "\t\t\t<real>0.0</real>\r\n" \
+ "\t\t</dict>\r\n" \
+ "</array>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define PLAYBACK_INFO_NOT_READY "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
+"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
+"<plist version=\"1.0\">\r\n"\
+"<dict>\r\n"\
+"<key>readyToPlay</key>\r\n"\
+"<false/>\r\n"\
+"</dict>\r\n"\
+"</plist>\r\n"
+
+#define SERVER_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>deviceid</key>\r\n" \
+ "<string>{:s}</string>\r\n" \
+ "<key>features</key>\r\n" \
+ "<integer>119</integer>\r\n" \
+ "<key>model</key>\r\n" \
+ "<string>Kodi,1</string>\r\n" \
+ "<key>protovers</key>\r\n" \
+ "<string>1.0</string>\r\n" \
+ "<key>srcvers</key>\r\n" \
+ "<string>" AIRPLAY_SERVER_VERSION_STR "</string>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define EVENT_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>category</key>\r\n" \
+ "<string>video</string>\r\n" \
+ "<key>sessionID</key>\r\n" \
+ "<integer>{:d}</integer>\r\n" \
+ "<key>state</key>\r\n" \
+ "<string>{:s}</string>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define AUTH_REALM "AirPlay"
+#define AUTH_REQUIRED "WWW-Authenticate: Digest realm=\"" AUTH_REALM "\", nonce=\"{:s}\"\r\n"
+
+void CAirPlayServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ // We are only interested in player changes
+ if ((flag & ANNOUNCEMENT::Player) == 0)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ if (sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER && ServerInstance)
+ {
+ if (message == "OnStop")
+ {
+ bool shouldRestoreVolume = true;
+ if (data.isMember("player") && data["player"].isMember("playerid"))
+ shouldRestoreVolume = (data["player"]["playerid"] != PLAYLIST::TYPE_PICTURE);
+
+ if (shouldRestoreVolume)
+ restoreVolume();
+
+ ServerInstance->AnnounceToClients(EVENT_STOPPED);
+ }
+ else if (message == "OnPlay" || message == "OnResume")
+ {
+ ServerInstance->AnnounceToClients(EVENT_PLAYING);
+ }
+ else if (message == "OnPause")
+ {
+ ServerInstance->AnnounceToClients(EVENT_PAUSED);
+ }
+ }
+}
+
+bool CAirPlayServer::StartServer(int port, bool nonlocal)
+{
+ StopServer(true);
+
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ ServerInstance = new CAirPlayServer(port, nonlocal);
+ if (ServerInstance->Initialize())
+ {
+ ServerInstance->Create();
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CAirPlayServer::SetCredentials(bool usePassword, const std::string& password)
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+ bool ret = false;
+
+ if (ServerInstance)
+ {
+ ret = ServerInstance->SetInternalCredentials(usePassword, password);
+ }
+ return ret;
+}
+
+bool CAirPlayServer::SetInternalCredentials(bool usePassword, const std::string& password)
+{
+ m_usePassword = usePassword;
+ m_password = password;
+ return true;
+}
+
+void ClearPhotoAssetCache()
+{
+ CLog::Log(LOGINFO, "AIRPLAY: Cleaning up photoassetcache");
+ // remove all cached photos
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory("special://temp/", items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+ if (!pItem->m_bIsFolder)
+ {
+ if (StringUtils::StartsWithNoCase(pItem->GetLabel(), "airplayasset") &&
+ (StringUtils::EndsWithNoCase(pItem->GetLabel(), ".jpg") ||
+ StringUtils::EndsWithNoCase(pItem->GetLabel(), ".png") ))
+ {
+ XFILE::CFile::Delete(pItem->GetPath());
+ }
+ }
+ }
+}
+
+void CAirPlayServer::StopServer(bool bWait)
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+ //clean up the photo cache temp folder
+ ClearPhotoAssetCache();
+
+ if (ServerInstance)
+ {
+ ServerInstance->StopThread(bWait);
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+ }
+}
+
+bool CAirPlayServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return static_cast<CThread*>(ServerInstance)->IsRunning();
+}
+
+void CAirPlayServer::AnnounceToClients(int state)
+{
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+
+ for (auto& it : m_connections)
+ {
+ std::string reverseHeader;
+ std::string reverseBody;
+ std::string response;
+ int reverseSocket = INVALID_SOCKET;
+ it.ComposeReverseEvent(reverseHeader, reverseBody, state);
+
+ // Send event status per reverse http socket (play, loading, paused)
+ // if we have a reverse header and a reverse socket
+ if (!reverseHeader.empty() && m_reverseSockets.find(it.m_sessionId) != m_reverseSockets.end())
+ {
+ //search the reverse socket to this sessionid
+ response = StringUtils::Format("POST /event HTTP/1.1\r\n");
+ reverseSocket = m_reverseSockets[it.m_sessionId]; //that is our reverse socket
+ response += reverseHeader;
+ }
+ response += "\r\n";
+
+ if (!reverseBody.empty())
+ {
+ response += reverseBody;
+ }
+
+ // don't send it to the connection object
+ // the reverse socket itself belongs to
+ if (reverseSocket != INVALID_SOCKET && reverseSocket != it.m_socket)
+ {
+ send(reverseSocket, response.c_str(), response.size(), 0);//send the event status on the eventSocket
+ }
+ }
+}
+
+CAirPlayServer::CAirPlayServer(int port, bool nonlocal) : CThread("AirPlayServer")
+{
+ m_port = port;
+ m_nonlocal = nonlocal;
+ m_ServerSockets = std::vector<SOCKET>();
+ m_usePassword = false;
+ m_origVolume = -1;
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+CAirPlayServer::~CAirPlayServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+void handleZeroconfAnnouncement()
+{
+#if defined(HAS_ZEROCONF)
+ static XbmcThreads::EndTime<> timeout(10s);
+ if(timeout.IsTimePast())
+ {
+ CZeroconf::GetInstance()->ForceReAnnounceService("servers.airplay");
+ timeout.Set(10s);
+ }
+#endif
+}
+
+void CAirPlayServer::Process()
+{
+ m_bStop = false;
+ static int sessionCounter = 0;
+
+ while (!m_bStop)
+ {
+ int max_fd = 0;
+ fd_set rfds;
+ struct timeval to = {1, 0};
+ FD_ZERO(&rfds);
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ FD_SET(socket, &rfds);
+ if ((intptr_t)socket > (intptr_t)max_fd)
+ max_fd = socket;
+ }
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ FD_SET(m_connections[i].m_socket, &rfds);
+ if (m_connections[i].m_socket > max_fd)
+ max_fd = m_connections[i].m_socket;
+ }
+
+ int res = select(max_fd+1, &rfds, NULL, NULL, &to);
+ if (res < 0)
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: Select failed");
+ CThread::Sleep(1000ms);
+ Initialize();
+ }
+ else if (res > 0)
+ {
+ for (int i = m_connections.size() - 1; i >= 0; i--)
+ {
+ int socket = m_connections[i].m_socket;
+ if (FD_ISSET(socket, &rfds))
+ {
+ char buffer[RECEIVEBUFFER] = {};
+ int nread = 0;
+ nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
+ if (nread > 0)
+ {
+ std::string sessionId;
+ m_connections[i].PushBuffer(this, buffer, nread, sessionId, m_reverseSockets);
+ }
+ if (nread <= 0)
+ {
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ CLog::Log(LOGINFO, "AIRPLAY Server: Disconnection detected");
+ m_connections[i].Disconnect();
+ m_connections.erase(m_connections.begin() + i);
+ }
+ }
+ }
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ if (FD_ISSET(socket, &rfds))
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY Server: New connection detected");
+ CTCPClient newconnection;
+ newconnection.m_socket = accept(socket, (struct sockaddr*) &newconnection.m_cliaddr, &newconnection.m_addrlen);
+ sessionCounter++;
+ newconnection.m_sessionCounter = sessionCounter;
+
+ if (newconnection.m_socket == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: Accept of new connection failed: {}", errno);
+ if (EBADF == errno)
+ {
+ CThread::Sleep(1000ms);
+ Initialize();
+ break;
+ }
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ CLog::Log(LOGINFO, "AIRPLAY Server: New connection added");
+ m_connections.push_back(newconnection);
+ }
+ }
+ }
+ }
+
+ // by reannouncing the zeroconf service
+ // we fix issues where xbmc is detected
+ // as audio-only target on devices with
+ // ios7 and later
+ handleZeroconfAnnouncement();
+ }
+
+ Deinitialize();
+}
+
+bool CAirPlayServer::Initialize()
+{
+ Deinitialize();
+
+ m_ServerSockets = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "AIRPLAY");
+ if (m_ServerSockets.empty())
+ return false;
+
+ CLog::Log(LOGINFO, "AIRPLAY Server: Successfully initialized");
+ return true;
+}
+
+void CAirPlayServer::Deinitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ m_connections[i].Disconnect();
+
+ m_connections.clear();
+ m_reverseSockets.clear();
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ shutdown(socket, SHUT_RDWR);
+ close(socket);
+ }
+ m_ServerSockets.clear();
+}
+
+CAirPlayServer::CTCPClient::CTCPClient()
+{
+ m_socket = INVALID_SOCKET;
+ m_httpParser = new HttpParser();
+
+ m_addrlen = sizeof(struct sockaddr_storage);
+
+ m_bAuthenticated = false;
+ m_lastEvent = EVENT_NONE;
+}
+
+CAirPlayServer::CTCPClient::CTCPClient(const CTCPClient& client)
+: m_lastEvent(EVENT_NONE)
+{
+ Copy(client);
+ m_httpParser = new HttpParser();
+}
+
+CAirPlayServer::CTCPClient::~CTCPClient()
+{
+ delete m_httpParser;
+}
+
+CAirPlayServer::CTCPClient& CAirPlayServer::CTCPClient::operator=(const CTCPClient& client)
+{
+ Copy(client);
+ m_httpParser = new HttpParser();
+ return *this;
+}
+
+void CAirPlayServer::CTCPClient::PushBuffer(CAirPlayServer *host, const char *buffer,
+ int length, std::string &sessionId, std::map<std::string,
+ int> &reverseSockets)
+{
+ HttpParser::status_t status = m_httpParser->addBytes(buffer, length);
+
+ if (status == HttpParser::Done)
+ {
+ // Parse the request
+ std::string responseHeader;
+ std::string responseBody;
+ int status = ProcessRequest(responseHeader, responseBody);
+ sessionId = m_sessionId;
+ std::string statusMsg = "OK";
+
+ switch(status)
+ {
+ case AIRPLAY_STATUS_NOT_IMPLEMENTED:
+ statusMsg = "Not Implemented";
+ break;
+ case AIRPLAY_STATUS_SWITCHING_PROTOCOLS:
+ statusMsg = "Switching Protocols";
+ reverseSockets[sessionId] = m_socket;//save this socket as reverse http socket for this sessionid
+ break;
+ case AIRPLAY_STATUS_NEED_AUTH:
+ statusMsg = "Unauthorized";
+ break;
+ case AIRPLAY_STATUS_NOT_FOUND:
+ statusMsg = "Not Found";
+ break;
+ case AIRPLAY_STATUS_METHOD_NOT_ALLOWED:
+ statusMsg = "Method Not Allowed";
+ break;
+ case AIRPLAY_STATUS_PRECONDITION_FAILED:
+ statusMsg = "Precondition Failed";
+ break;
+ }
+
+ // Prepare the response
+ std::string response;
+ const time_t ltime = time(NULL);
+ char *date = asctime(gmtime(&ltime)); //Fri, 17 Dec 2010 11:18:01 GMT;
+ date[strlen(date) - 1] = '\0'; // remove \n
+ response = StringUtils::Format("HTTP/1.1 {} {}\nDate: {}\r\n", status, statusMsg, date);
+ if (!responseHeader.empty())
+ {
+ response += responseHeader;
+ }
+
+ response = StringUtils::Format("{}Content-Length: {}\r\n\r\n", response, responseBody.size());
+
+ if (!responseBody.empty())
+ {
+ response += responseBody;
+ }
+
+ // Send the response
+ //don't send response on AIRPLAY_STATUS_NO_RESPONSE_NEEDED
+ if (status != AIRPLAY_STATUS_NO_RESPONSE_NEEDED)
+ {
+ send(m_socket, response.c_str(), response.size(), 0);
+ }
+ // We need a new parser...
+ delete m_httpParser;
+ m_httpParser = new HttpParser;
+ }
+}
+
+void CAirPlayServer::CTCPClient::Disconnect()
+{
+ if (m_socket != INVALID_SOCKET)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ shutdown(m_socket, SHUT_RDWR);
+ close(m_socket);
+ m_socket = INVALID_SOCKET;
+ delete m_httpParser;
+ m_httpParser = NULL;
+ }
+}
+
+void CAirPlayServer::CTCPClient::Copy(const CTCPClient& client)
+{
+ m_socket = client.m_socket;
+ m_cliaddr = client.m_cliaddr;
+ m_addrlen = client.m_addrlen;
+ m_httpParser = client.m_httpParser;
+ m_authNonce = client.m_authNonce;
+ m_bAuthenticated = client.m_bAuthenticated;
+ m_sessionCounter = client.m_sessionCounter;
+}
+
+
+void CAirPlayServer::CTCPClient::ComposeReverseEvent( std::string& reverseHeader,
+ std::string& reverseBody,
+ int state)
+{
+
+ if ( m_lastEvent != state )
+ {
+ switch(state)
+ {
+ case EVENT_PLAYING:
+ case EVENT_LOADING:
+ case EVENT_PAUSED:
+ case EVENT_STOPPED:
+ reverseBody = StringUtils::Format(EVENT_INFO, m_sessionCounter, eventStrings[state]);
+ CLog::Log(LOGDEBUG, "AIRPLAY: sending event: {}", eventStrings[state]);
+ break;
+ }
+ reverseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ reverseHeader =
+ StringUtils::Format("{}Content-Length: {}\r\n", reverseHeader, reverseBody.size());
+ reverseHeader = StringUtils::Format("{}x-apple-session-id: {}\r\n", reverseHeader.c_str(),
+ m_sessionId.c_str());
+ m_lastEvent = state;
+ }
+}
+
+void CAirPlayServer::CTCPClient::ComposeAuthRequestAnswer(std::string& responseHeader, std::string& responseBody)
+{
+ int16_t random=rand();
+ std::string randomStr = std::to_string(random);
+ m_authNonce=CDigest::Calculate(CDigest::Type::MD5, randomStr);
+ responseHeader = StringUtils::Format(AUTH_REQUIRED, m_authNonce);
+ responseBody.clear();
+}
+
+
+//as of rfc 2617
+std::string calcResponse(const std::string& username,
+ const std::string& password,
+ const std::string& realm,
+ const std::string& method,
+ const std::string& digestUri,
+ const std::string& nonce)
+{
+ std::string response;
+ std::string HA1;
+ std::string HA2;
+
+ HA1 = CDigest::Calculate(CDigest::Type::MD5, username + ":" + realm + ":" + password);
+ HA2 = CDigest::Calculate(CDigest::Type::MD5, method + ":" + digestUri);
+ response = CDigest::Calculate(CDigest::Type::MD5, HA1 + ":" + nonce + ":" + HA2);
+ return response;
+}
+
+//helper function
+//from a string field1="value1", field2="value2" it parses the value to a field
+std::string getFieldFromString(const std::string &str, const char* field)
+{
+ std::vector<std::string> tmpAr1 = StringUtils::Split(str, ",");
+ for (const auto& i : tmpAr1)
+ {
+ if (i.find(field) != std::string::npos)
+ {
+ std::vector<std::string> tmpAr2 = StringUtils::Split(i, "=");
+ if (tmpAr2.size() == 2)
+ {
+ StringUtils::Replace(tmpAr2[1], "\"", "");//remove quotes
+ return tmpAr2[1];
+ }
+ }
+ }
+ return "";
+}
+
+bool CAirPlayServer::CTCPClient::checkAuthorization(const std::string& authStr,
+ const std::string& method,
+ const std::string& uri)
+{
+ bool authValid = true;
+
+ std::string username;
+
+ if (authStr.empty())
+ return false;
+
+ //first get username - we allow all usernames for airplay (usually it is AirPlay)
+ username = getFieldFromString(authStr, "username");
+ if (username.empty())
+ {
+ authValid = false;
+ }
+
+ //second check realm
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "realm") != AUTH_REALM)
+ {
+ authValid = false;
+ }
+ }
+
+ //third check nonce
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "nonce") != m_authNonce)
+ {
+ authValid = false;
+ }
+ }
+
+ //forth check uri
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "uri") != uri)
+ {
+ authValid = false;
+ }
+ }
+
+ //last check response
+ if (authValid)
+ {
+ std::string realm = AUTH_REALM;
+ std::string ourResponse = calcResponse(username, ServerInstance->m_password, realm, method, uri, m_authNonce);
+ std::string theirResponse = getFieldFromString(authStr, "response");
+ if (!StringUtils::EqualsNoCase(theirResponse, ourResponse))
+ {
+ authValid = false;
+ CLog::Log(LOGDEBUG, "AirAuth: response mismatch - our: {} theirs: {}", ourResponse,
+ theirResponse);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "AirAuth: successful authentication from AirPlay client");
+ }
+ }
+ m_bAuthenticated = authValid;
+ return m_bAuthenticated;
+}
+
+void CAirPlayServer::backupVolume()
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ if (ServerInstance && ServerInstance->m_origVolume == -1)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ ServerInstance->m_origVolume = static_cast<int>(appVolume->GetVolumePercent());
+ }
+}
+
+void CAirPlayServer::restoreVolume()
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (ServerInstance && ServerInstance->m_origVolume != -1 &&
+ settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(static_cast<float>(ServerInstance->m_origVolume));
+ ServerInstance->m_origVolume = -1;
+ }
+}
+
+std::string getStringFromPlist(plist_t node)
+{
+ std::string ret;
+ char *tmpStr = nullptr;
+ plist_get_string_val(node, &tmpStr);
+ ret = tmpStr;
+ free(tmpStr);
+ return ret;
+}
+
+int CAirPlayServer::CTCPClient::ProcessRequest( std::string& responseHeader,
+ std::string& responseBody)
+{
+ std::string method = m_httpParser->getMethod() ? m_httpParser->getMethod() : "";
+ std::string uri = m_httpParser->getUri() ? m_httpParser->getUri() : "";
+ std::string queryString = m_httpParser->getQueryString() ? m_httpParser->getQueryString() : "";
+ std::string body = m_httpParser->getBody() ? m_httpParser->getBody() : "";
+ std::string contentType = m_httpParser->getValue("content-type") ? m_httpParser->getValue("content-type") : "";
+ m_sessionId = m_httpParser->getValue("x-apple-session-id") ? m_httpParser->getValue("x-apple-session-id") : "";
+ std::string authorization = m_httpParser->getValue("authorization") ? m_httpParser->getValue("authorization") : "";
+ std::string photoAction = m_httpParser->getValue("x-apple-assetaction") ? m_httpParser->getValue("x-apple-assetaction") : "";
+ std::string photoCacheId = m_httpParser->getValue("x-apple-assetkey") ? m_httpParser->getValue("x-apple-assetkey") : "";
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int status = AIRPLAY_STATUS_OK;
+ bool needAuth = false;
+
+ if (m_sessionId.empty())
+ m_sessionId = "00000000-0000-0000-0000-000000000000";
+
+ if (ServerInstance->m_usePassword && !m_bAuthenticated)
+ {
+ needAuth = true;
+ }
+
+ size_t startQs = uri.find('?');
+ if (startQs != std::string::npos)
+ {
+ uri.erase(startQs);
+ }
+
+ // This is the socket which will be used for reverse HTTP
+ // negotiate reverse HTTP via upgrade
+ if (uri == "/reverse")
+ {
+ status = AIRPLAY_STATUS_SWITCHING_PROTOCOLS;
+ responseHeader = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
+ }
+
+ // The rate command is used to play/pause media.
+ // A value argument should be supplied which indicates media should be played or paused.
+ // 0.000000 => pause
+ // 1.000000 => play
+ else if (uri == "/rate")
+ {
+ const char* found = strstr(queryString.c_str(), "value=");
+ int rate = found ? (int)(atof(found + strlen("value=")) + 0.5) : 0;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {} with rate {}", uri, rate);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (rate == 0)
+ {
+ if (appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ }
+ else
+ {
+ if (appPlayer->IsPausedPlayback())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ }
+ }
+
+ // The volume command is used to change playback volume.
+ // A value argument should be supplied which indicates how loud we should get.
+ // 0.000000 => silent
+ // 1.000000 => loud
+ else if (uri == "/volume")
+ {
+ const char* found = strstr(queryString.c_str(), "volume=");
+ float volume = found ? (float)strtod(found + strlen("volume="), NULL) : 0;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {} with volume {:f}", uri, volume);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (volume >= 0 && volume <= 1)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ float oldVolume = appVolume->GetVolumePercent();
+ volume *= 100;
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (oldVolume != volume &&
+ settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ backupVolume();
+ appVolume->SetVolume(volume);
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_VOLUME_SHOW, oldVolume < volume ? ACTION_VOLUME_UP : ACTION_VOLUME_DOWN);
+ }
+ }
+ }
+
+
+ // Contains a header like format in the request body which should contain a
+ // Content-Location and optionally a Start-Position
+ else if (uri == "/play")
+ {
+ std::string location;
+ float position = 0.0;
+ bool startPlayback = true;
+ m_lastEvent = EVENT_NONE;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (contentType == "application/x-apple-binary-plist")
+ {
+ CAirPlayServer::m_isPlaying++;
+
+ const char* bodyChr = m_httpParser->getBody();
+
+ plist_t dict = NULL;
+ plist_from_bin(bodyChr, m_httpParser->getContentLength(), &dict);
+
+ if (plist_dict_get_size(dict))
+ {
+ plist_t tmpNode = plist_dict_get_item(dict, "Start-Position");
+ if (tmpNode)
+ {
+ double tmpDouble = 0;
+ plist_get_real_val(tmpNode, &tmpDouble);
+ position = (float)tmpDouble;
+ }
+
+ tmpNode = plist_dict_get_item(dict, "Content-Location");
+ if (tmpNode)
+ {
+ location = getStringFromPlist(tmpNode);
+ tmpNode = NULL;
+ }
+
+ tmpNode = plist_dict_get_item(dict, "rate");
+ if (tmpNode)
+ {
+ double rate = 0;
+ plist_get_real_val(tmpNode, &rate);
+ if (rate == 0.0)
+ {
+ startPlayback = false;
+ }
+ tmpNode = NULL;
+ }
+
+ // in newer protocol versions the location is given
+ // via host and path where host is ip:port and path is /path/file.mov
+ if (location.empty())
+ tmpNode = plist_dict_get_item(dict, "host");
+ if (tmpNode)
+ {
+ location = "http://";
+ location += getStringFromPlist(tmpNode);
+
+ tmpNode = plist_dict_get_item(dict, "path");
+ if (tmpNode)
+ {
+ location += getStringFromPlist(tmpNode);
+ }
+ }
+
+ if (dict)
+ {
+ plist_free(dict);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Error parsing plist");
+ }
+ }
+ else
+ {
+ CAirPlayServer::m_isPlaying++;
+ // Get URL to play
+ std::string contentLocation = "Content-Location: ";
+ size_t start = body.find(contentLocation);
+ if (start == std::string::npos)
+ return AIRPLAY_STATUS_NOT_IMPLEMENTED;
+ start += contentLocation.size();
+ int end = body.find('\n', start);
+ location = body.substr(start, end - start);
+
+ std::string startPosition = "Start-Position: ";
+ start = body.find(startPosition);
+ if (start != std::string::npos)
+ {
+ start += startPosition.size();
+ int end = body.find('\n', start);
+ std::string positionStr = body.substr(start, end - start);
+ position = (float)atof(positionStr.c_str());
+ }
+ }
+
+ if (status != AIRPLAY_STATUS_NEED_AUTH)
+ {
+ std::string userAgent(CURL::Encode("AppleCoreMedia/1.0.0.8F455 (AppleTV; U; CPU OS 4_3 like Mac OS X; de_de)"));
+ location += "|User-Agent=" + userAgent;
+
+ CFileItem fileToPlay(location, false);
+ fileToPlay.SetProperty("StartPercent", position*100.0f);
+ ServerInstance->AnnounceToClients(EVENT_LOADING);
+
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(fileToPlay));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l));
+
+ // allow starting the player paused in ios8 mode (needed by camera roll app)
+ if (!startPlayback)
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ appPlayer->SeekPercentage(position * 100.0f);
+ }
+ }
+ }
+
+ // Used to perform seeking (POST request) and to retrieve current player position (GET request).
+ // GET scrub seems to also set rate 1 - strange but true
+ else if (uri == "/scrub")
+ {
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (method == "GET")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got GET request {}", uri);
+
+ if (appPlayer->GetTotalTime())
+ {
+ float position = static_cast<float>(appPlayer->GetTime()) / 1000;
+ responseBody =
+ StringUtils::Format("duration: {:.6f}\r\nposition: {:.6f}\r\n",
+ static_cast<float>(appPlayer->GetTotalTime()) / 1000, position);
+ }
+ else
+ {
+ status = AIRPLAY_STATUS_METHOD_NOT_ALLOWED;
+ }
+ }
+ else
+ {
+ const char* found = strstr(queryString.c_str(), "position=");
+
+ if (found && appPlayer->HasPlayer())
+ {
+ int64_t position = (int64_t) (atof(found + strlen("position=")) * 1000.0);
+ appPlayer->SeekTime(position);
+ CLog::Log(LOGDEBUG, "AIRPLAY: got POST request {} with pos {}", uri, position);
+ }
+ }
+ }
+
+ // Sent when media playback should be stopped
+ else if (uri == "/stop")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else
+ {
+ if (IsPlaying()) //only stop player if we started him
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ CAirPlayServer::m_isPlaying--;
+ }
+ else //if we are not playing and get the stop request - we just wanna stop picture streaming
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+ }
+ }
+ ClearPhotoAssetCache();
+ }
+
+ // RAW JPEG data is contained in the request body
+ else if (uri == "/photo")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (m_httpParser->getContentLength() > 0 || photoAction == "displayCached")
+ {
+ XFILE::CFile tmpFile;
+ std::string tmpFileName = "special://temp/airplayasset";
+ bool showPhoto = true;
+ bool receivePhoto = true;
+
+
+ if (photoAction == "cacheOnly")
+ showPhoto = false;
+ else if (photoAction == "displayCached")
+ {
+ receivePhoto = false;
+ if (photoCacheId.length())
+ CLog::Log(LOGDEBUG, "AIRPLAY: Trying to show from cache asset: {}", photoCacheId);
+ }
+
+ if (photoCacheId.length())
+ tmpFileName += photoCacheId;
+ else
+ tmpFileName += "airplay_photo";
+
+ if( receivePhoto && m_httpParser->getContentLength() > 3 &&
+ m_httpParser->getBody()[1] == 'P' &&
+ m_httpParser->getBody()[2] == 'N' &&
+ m_httpParser->getBody()[3] == 'G')
+ {
+ tmpFileName += ".png";
+ }
+ else
+ {
+ tmpFileName += ".jpg";
+ }
+
+ int writtenBytes=0;
+ if (receivePhoto)
+ {
+ if (tmpFile.OpenForWrite(tmpFileName, true))
+ {
+ writtenBytes = tmpFile.Write(m_httpParser->getBody(), m_httpParser->getContentLength());
+ tmpFile.Close();
+ }
+ if (photoCacheId.length())
+ CLog::Log(LOGDEBUG, "AIRPLAY: Cached asset: {}", photoCacheId);
+ }
+
+ if (showPhoto)
+ {
+ if ((writtenBytes > 0 && (unsigned int)writtenBytes == m_httpParser->getContentLength()) || !receivePhoto)
+ {
+ if (!receivePhoto && !XFILE::CFile::Exists(tmpFileName))
+ {
+ status = AIRPLAY_STATUS_PRECONDITION_FAILED; //image not found in the cache
+ if (photoCacheId.length())
+ CLog::Log(LOGWARNING, "AIRPLAY: Asset {} not found in our cache.", photoCacheId);
+ }
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PICTURE_SHOW, -1, -1, nullptr,
+ tmpFileName);
+ }
+ else
+ {
+ CLog::Log(LOGERROR,"AirPlayServer: Error writing tmpFile.");
+ }
+ }
+ }
+ }
+
+ else if (uri == "/playback-info")
+ {
+ float position = 0.0f;
+ float duration = 0.0f;
+ float cachePosition = 0.0f;
+ bool playing = false;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (appPlayer->HasPlayer())
+ {
+ if (appPlayer->GetTotalTime())
+ {
+ position = static_cast<float>(appPlayer->GetTime()) / 1000;
+ duration = static_cast<float>(appPlayer->GetTotalTime()) / 1000;
+ playing = !appPlayer->IsPaused();
+ cachePosition = position + (duration * appPlayer->GetCachePercentage() / 100.0f);
+ }
+
+ responseBody = StringUtils::Format(PLAYBACK_INFO, duration, cachePosition, position, (playing ? 1 : 0), duration);
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+
+ if (appPlayer->IsCaching())
+ {
+ CAirPlayServer::ServerInstance->AnnounceToClients(EVENT_LOADING);
+ }
+ }
+ else
+ {
+ responseBody = StringUtils::Format(PLAYBACK_INFO_NOT_READY);
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ }
+ }
+
+ else if (uri == "/server-info")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ responseBody = StringUtils::Format(
+ SERVER_INFO, CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetMacAddress());
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ }
+
+ else if (uri == "/slideshow-features")
+ {
+ // Ignore for now.
+ }
+
+ else if (uri == "/authorize")
+ {
+ // DRM, ignore for now.
+ }
+
+ else if (uri == "/setProperty")
+ {
+ status = AIRPLAY_STATUS_NOT_FOUND;
+ }
+
+ else if (uri == "/getProperty")
+ {
+ status = AIRPLAY_STATUS_NOT_FOUND;
+ }
+
+ else if (uri == "/fp-setup")
+ {
+ status = AIRPLAY_STATUS_PRECONDITION_FAILED;
+ }
+
+ else if (uri == "200") //response OK from the event reverse message
+ {
+ status = AIRPLAY_STATUS_NO_RESPONSE_NEEDED;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: unhandled request [{}]", uri);
+ status = AIRPLAY_STATUS_NOT_IMPLEMENTED;
+ }
+
+ if (status == AIRPLAY_STATUS_NEED_AUTH)
+ {
+ ComposeAuthRequestAnswer(responseHeader, responseBody);
+ }
+
+ return status;
+}