123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- //
- // Created by Tobias Hieta on 24/03/15.
- //
- #include "RemoteComponent.h"
- #include <QXmlStreamWriter>
- #include <QUrlQuery>
- #include "QsLog.h"
- #include "settings/SettingsComponent.h"
- #include "utils/Utils.h"
- #include "Version.h"
- static QMap<QString, QString> g_resourceKeyMap = {
- { "Name", "title" },
- { "Resource-Identifier", "machineIdentifier" },
- { "Product", "product" },
- { "Version", "version" },
- { "Protocol-Version", "protocolVersion" },
- { "Protocol-Capabilities", "protocolCapabilities" },
- { "Device-Class", "deviceClass" }
- };
- static QMap<QString, QString> g_headerKeyMap = {
- { "Name", "X-Plex-Device-Name" },
- { "Resource-Identifier", "X-Plex-Client-Identifier" },
- { "Product", "X-Plex-Product" },
- { "Version", "X-Plex-Version" }
- };
- /////////////////////////////////////////////////////////////////////////////////////////
- RemoteComponent::RemoteComponent(QObject* parent) : ComponentBase(parent), m_commandId(0)
- {
- m_gdmManager = new GDMManager(this);
- m_networkAccessManager = new QNetworkAccessManager(this);
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- bool RemoteComponent::componentInitialize()
- {
- m_gdmManager->startAnnouncing();
- // check for timed out subscribers
- m_subscriberTimer.setInterval(5000);
- connect(&m_subscriberTimer, &QTimer::timeout, this, &RemoteComponent::checkSubscribers);
- m_subscriberTimer.start();
- // connect the network access stuff
- connect(m_networkAccessManager, &QNetworkAccessManager::finished, this, &RemoteComponent::timelineFinished);
- return true;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- QVariantMap RemoteComponent::HeaderInformation()
- {
- QVariantMap gdmInfo = GDMInformation();
- QVariantMap headerInfo;
- for(const QString& key : gdmInfo.keys())
- {
- if (g_headerKeyMap.contains(key))
- headerInfo[g_headerKeyMap[key]] = gdmInfo[key];
- }
- headerInfo["X-Plex-Platform"] = QSysInfo::productType();
- headerInfo["X-Plex-Platform-Version"] = QSysInfo::productVersion();
-
- return headerInfo;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- QVariantMap RemoteComponent::ResourceInformation()
- {
- QVariantMap gdmInfo = GDMInformation();
- QVariantMap resourceInfo;
- for(const QString& key : gdmInfo.keys())
- {
- if (g_resourceKeyMap.contains(key))
- resourceInfo[g_resourceKeyMap[key]] = gdmInfo[key];
- }
- resourceInfo["platform"] = QSysInfo::productType();
- resourceInfo["platformVersion"] = QSysInfo::productVersion();
- return resourceInfo;
- };
- /////////////////////////////////////////////////////////////////////////////////////////
- QVariantMap RemoteComponent::GDMInformation()
- {
- QVariantMap headers = {
- {"Name", Utils::ComputerName()},
- {"Port", SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "webserverport")},
- {"Version", Version::GetVersionString()},
- {"Product", "Plex Media Player"},
- {"Protocol", "plex"},
- {"Protocol-Version", "1"},
- {"Protocol-Capabilities", "navigation,playback,timeline,mirror,playqueues"},
- {"Device-Class", "pc"},
- {"Resource-Identifier", SettingsComponent::Get().value(SETTINGS_SECTION_WEBCLIENT, "clientID")}
- };
- return headers;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::handleResource(QHttpRequest* request, QHttpResponse* response)
- {
- if (request->method() == qhttp::EHTTP_GET)
- {
- QVariantMap headers = ResourceInformation();
- QByteArray outputData;
- QXmlStreamWriter output(&outputData);
- output.setAutoFormatting(true);
- output.writeStartDocument();
- output.writeStartElement("MediaContainer");
- output.writeStartElement("Player");
- for(const QString& key : headers.keys())
- output.writeAttribute(key, headers[key].toString());
- output.writeEndElement();
- output.writeEndDocument();
- response->setStatusCode(qhttp::ESTATUS_OK);
- response->write(outputData);
- response->end();
- }
- else
- {
- response->setStatusCode(qhttp::ESTATUS_METHOD_NOT_ALLOWED);
- response->end();
- }
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- QVariantMap RemoteComponent::QueryToMap(const QUrl& url)
- {
- QUrlQuery query(url);
- QVariantMap queryMap;
- for(auto stringPair : query.queryItems())
- {
- QString key = stringPair.first;
- QString value = stringPair.second;
- QVariantList l;
- if (queryMap.contains(key))
- {
- l = queryMap[key].toList();
- l.append(value);
- }
- else
- {
- l.append(value);
- }
- queryMap[key] = l;
- }
- return queryMap;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- QVariantMap RemoteComponent::HeaderToMap(const qhttp::THeaderHash& hash)
- {
- QVariantMap variantMap;
- for(const QString& key : hash.keys())
- variantMap.insert(key, hash.value(key.toUtf8()));
- return variantMap;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::handleCommand(QHttpRequest* request, QHttpResponse* response)
- {
- QVariantMap queryMap = QueryToMap(request->url());
- QVariantMap headerMap = HeaderToMap(request->headers());
- QString identifier = headerMap["x-plex-client-identifier"].toString();
- response->addHeader("Access-Control-Allow-Origin", "*");
- response->addHeader("X-Plex-Client-Identifier", SettingsComponent::Get().value(SETTINGS_SECTION_WEBCLIENT, "clientID").toByteArray());
- // handle CORS requests here
- if ((request->method() == qhttp::EHTTP_OPTIONS) && headerMap.contains("access-control-request-method"))
- {
- response->addHeader("Content-Type", "text/plain");
- response->addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, HEAD");
- response->addHeader("Access-Control-Max-Age", "1209600");
- response->addHeader("Connection", "close");
- if (headerMap.contains("access-control-request-headers"))
- {
- response->addHeader("Access-Control-Allow-Headers", headerMap.value("access-control-request-headers").toByteArray());
- }
- response->setStatusCode(qhttp::ESTATUS_OK);
- response->end();
- return;
- }
- // we want to handle the subscription events in the host
- // since we are going to handle the updating later.
- //
- if (request->url().path() == "/player/timeline/subscribe")
- {
- handleSubscription(request, response, false);
- return;
- }
- else if (request->url().path() == "/player/timeline/unsubscribe")
- {
- subscriberRemove(request->headers()["x-plex-client-identifier"]);
- response->setStatusCode(qhttp::ESTATUS_OK);
- response->end();
- return;
- }
- else if ((request->url().path() == "/player/timeline/poll"))
- {
- if (!m_subscriberMap.contains(identifier))
- handleSubscription(request, response, true);
- RemotePollSubscriber *subscriber = (RemotePollSubscriber *)m_subscriberMap[identifier];
- if (subscriber)
- {
- subscriber->reSubscribe();
- subscriber->setHTTPResponse(response);
- // if we don't have to wait, just ship the update right away
- // otherwise, this will wait until next update
- if (! (queryMap.contains("wait") && (queryMap["wait"].toList()[0].toInt() == 1)))
- {
- subscriber->sendUpdate();
- }
- }
- return;
- }
- // handle commandID
- if (!headerMap.contains("x-plex-client-identifier") || !queryMap.contains("commandID"))
- {
- QLOG_WARN() << "Can't find a X-Plex-Client-Identifier header";
- response->setStatusCode(qhttp::ESTATUS_NOT_ACCEPTABLE);
- response->end();
- return;
- }
- quint64 commandId = 0;
- {
- QMutexLocker lk(&m_responseLock);
- commandId = ++m_commandId;
- m_responseMap[commandId] = response;
- connect(response, &QHttpResponse::done, this, &RemoteComponent::responseDone);
- }
- {
- QMutexLocker lk(&m_subscriberLock);
- if (!m_subscriberMap.contains(identifier))
- {
- QLOG_WARN() << "Failed to lock up subscriber" << identifier;
- response->setStatusCode(qhttp::ESTATUS_NOT_ACCEPTABLE);
- response->end();
- return;
- }
- RemoteSubscriber* subscriber = m_subscriberMap[identifier];
- subscriber->setCommandId(m_commandId, queryMap["commandID"].toList()[0].toInt());
- }
- QVariantMap arg = {
- { "method", request->methodString() },
- { "headers", headerMap },
- { "path", request->url().path() },
- { "query", queryMap },
- { "commandID", m_commandId}
- };
- emit commandReceived(arg);
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::responseDone()
- {
- QHttpResponse* response = dynamic_cast<QHttpResponse*>(sender());
- if (response)
- {
- QMutexLocker lk(&m_responseLock);
- int foundId = -1;
- for(int responseId : m_responseMap.keys())
- {
- if (m_responseMap[responseId] == response)
- {
- foundId = responseId;
- break;
- }
- }
- if (foundId != -1)
- m_responseMap.remove(foundId);
- }
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::commandResponse(const QVariantMap& responseArguments)
- {
- // check for minimum requirements in the responseArguments
- if (!responseArguments.contains("commandID") ||
- !responseArguments.contains("responseCode"))
- {
- QLOG_WARN() << "responseArguments did not contain a commandId or responseCode";
- return;
- }
- quint64 commandId = responseArguments["commandID"].toULongLong();
- uint responseCode = responseArguments["responseCode"].toUInt();
- QMutexLocker lk(&m_responseLock);
- if (!m_responseMap.contains(commandId))
- {
- QLOG_WARN() << "Could not find responseId:" << commandId << " - maybe it was removed because of a timeout?";
- return;
- }
- QHttpResponse* response = m_responseMap[commandId];
- // no need to hold the lock when we have changed m_responseMap
- lk.unlock();
- // add headers if we have them
- if (responseArguments.contains("headers") && responseArguments["headers"].type() == QVariant::Map)
- {
- QVariantMap headers = responseArguments["headers"].toMap();
- for(const QString& key : headers.keys())
- response->addHeader(key.toUtf8(), headers[key].toByteArray());
- }
- // write the response HTTP code
- response->setStatusCode((qhttp::TStatusCode)responseCode);
- // handle optional body argument
- if (responseArguments.contains("body") && responseArguments["body"].type() == QVariant::String)
- response->write(responseArguments["body"].toString().toUtf8());
- response->end();
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::handleSubscription(QHttpRequest* request, QHttpResponse* response, bool poll)
- {
- QVariantMap headers = HeaderToMap(request->headers());
- // check for required headers
- if (!headers.contains("x-plex-client-identifier") ||
- !headers.contains("x-plex-device-name"))
- {
- QLOG_ERROR() << "Missing X-Plex headers in /timeline/subscribe request";
- response->setStatusCode(qhttp::ESTATUS_BAD_REQUEST);
- response->end();
- return;
- }
- // check for required arguments
- QVariantMap query = QueryToMap(request->url());
- if (!query.contains("commandID") || ((!query.contains("port")) && !poll))
- {
- QLOG_ERROR() << "Missing arguments to /timeline/subscribe request";
- response->setStatusCode(qhttp::ESTATUS_BAD_REQUEST);
- response->end();
- return;
- }
- QString clientIdentifier(request->headers()["x-plex-client-identifier"]);
- QMutexLocker lk(&m_subscriberLock);
- RemoteSubscriber* subscriber = nullptr;
- if (m_subscriberMap.contains(clientIdentifier))
- {
- QLOG_DEBUG() << "Refreshed subscriber:" << clientIdentifier;
- subscriber = m_subscriberMap[clientIdentifier];
- subscriber->reSubscribe();
- }
- else
- {
- if (poll)
- {
- QLOG_DEBUG() << "New poll subscriber:" << clientIdentifier << request->headers()["x-plex-device-name"];
- subscriber = new RemotePollSubscriber(clientIdentifier, request->headers()["x-plex-device-name"], response, this);
- }
- else
- {
- QUrl address;
- QString protocol = query.contains("protocol") ? query["protocol"].toList()[0].toString() : "http";
- int port = query.contains("port") ? query["port"].toList()[0].toInt() : 32400;
- address.setScheme(protocol);
- address.setHost(request->remoteAddress());
- address.setPort(port);
- QLOG_DEBUG() << "New subscriber:" << clientIdentifier << request->headers()["x-plex-device-name"] << address.toString();
- subscriber = new RemoteSubscriber(clientIdentifier, request->headers()["x-plex-device-name"], address, this);
- }
- m_subscriberMap[clientIdentifier] = subscriber;
- // if it's our first controller, we notify web for subscription
- if (m_subscriberMap.size() == 1)
- {
- QLOG_DEBUG() << "First subscriber added, subscribing to web";
- subscribeToWeb(true);
- }
- }
- subscriber->setCommandId(m_commandId, query["commandID"].toList()[0].toInt());
- if (!poll)
- {
- subscriber->sendUpdate();
- response->setStatusCode(qhttp::ESTATUS_OK);
- response->end();
- }
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::subscribeToWeb(bool subscribe)
- {
- QVariantMap arg = {
- { "method", "GET" },
- { "path", "/player/timeline/" + (subscribe ? QLatin1String("subscribe") : QLatin1String("unsubscribe")) },
- { "commandID", m_commandId}
- };
- emit commandReceived(arg);
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::checkSubscribers()
- {
- QMutexLocker lk(&m_subscriberLock);
- QList<RemoteSubscriber*> subsToRemove;
- for(RemoteSubscriber* subscriber : m_subscriberMap.values())
- {
- // was it more than 10 seconds since this client checked in last?
- if (subscriber->lastSubscribe() > 90 * 1000)
- {
- QLOG_DEBUG() << "more than 10 seconds since we heard from:" << subscriber->deviceName() << "- unsubscribing..";
- subsToRemove << subscriber;
- }
- }
- lk.unlock();
- for(RemoteSubscriber* sub : subsToRemove)
- subscriberRemove(sub->clientIdentifier());
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- QNetworkAccessManager* RemoteComponent::getNetworkAccessManager()
- {
- // we might want to set common options here.
- return m_networkAccessManager;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::timelineFinished(QNetworkReply* reply)
- {
- QString identifier = reply->request().attribute(QNetworkRequest::User).toString();
- // ignore requests with no identifier
- if (identifier.isEmpty())
- return;
- QMutexLocker lk(&m_subscriberLock);
- if (!m_subscriberMap.contains(identifier))
- {
- QLOG_WARN() << "Got a networkreply with a identifier we don't know about:" << identifier;
- return;
- }
- RemoteSubscriber* sub = m_subscriberMap[identifier];
- lk.unlock();
- sub->timelineFinished(reply);
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::subscriberRemove(const QString& identifier)
- {
- QMutexLocker lk(&m_subscriberLock);
- if (!m_subscriberMap.contains(identifier))
- {
- QLOG_ERROR() << "Can't remove client:" << identifier << "since we don't know about it.";
- return;
- }
- RemoteSubscriber* subscriber = m_subscriberMap[identifier];
- m_subscriberMap.remove(identifier);
- subscriber->deleteLater();
- // if it's our first controller, we notify web for subscription
- if (m_subscriberMap.size() == 0)
- {
- QLOG_DEBUG() << "Last subscriber removed, unsubscribing from web";
- subscribeToWeb(false);
- }
- QLOG_DEBUG() << "Removed subscriber:" << identifier;
- }
- /////////////////////////////////////////////////////////////////////////////////////////
- void RemoteComponent::timelineUpdate(quint64 commandID, const QString& timeline)
- {
- QMutexLocker lk(&m_subscriberLock);
- for(RemoteSubscriber* subscriber : m_subscriberMap.values())
- {
- subscriber->queueTimeline(commandID, timeline.toUtf8());
- subscriber->sendUpdate();
- }
- }
|