RemoteComponent.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. //
  2. // Created by Tobias Hieta on 24/03/15.
  3. //
  4. #include "RemoteComponent.h"
  5. #include <QXmlStreamWriter>
  6. #include <QUrlQuery>
  7. #include "QsLog.h"
  8. #include "settings/SettingsComponent.h"
  9. #include "utils/Utils.h"
  10. #include "Version.h"
  11. static QMap<QString, QString> g_resourceKeyMap = {
  12. { "Name", "title" },
  13. { "Resource-Identifier", "machineIdentifier" },
  14. { "Product", "product" },
  15. { "Version", "version" },
  16. { "Protocol-Version", "protocolVersion" },
  17. { "Protocol-Capabilities", "protocolCapabilities" },
  18. { "Device-Class", "deviceClass" }
  19. };
  20. static QMap<QString, QString> g_headerKeyMap = {
  21. { "Name", "X-Plex-Device-Name" },
  22. { "Resource-Identifier", "X-Plex-Client-Identifier" },
  23. { "Product", "X-Plex-Product" },
  24. { "Version", "X-Plex-Version" }
  25. };
  26. /////////////////////////////////////////////////////////////////////////////////////////
  27. RemoteComponent::RemoteComponent(QObject* parent) : ComponentBase(parent), m_commandId(0)
  28. {
  29. m_gdmManager = new GDMManager(this);
  30. m_networkAccessManager = new QNetworkAccessManager(this);
  31. }
  32. /////////////////////////////////////////////////////////////////////////////////////////
  33. bool RemoteComponent::componentInitialize()
  34. {
  35. m_gdmManager->startAnnouncing();
  36. // check for timed out subscribers
  37. m_subscriberTimer.setInterval(5000);
  38. connect(&m_subscriberTimer, &QTimer::timeout, this, &RemoteComponent::checkSubscribers);
  39. m_subscriberTimer.start();
  40. // connect the network access stuff
  41. connect(m_networkAccessManager, &QNetworkAccessManager::finished, this, &RemoteComponent::timelineFinished);
  42. return true;
  43. }
  44. /////////////////////////////////////////////////////////////////////////////////////////
  45. QVariantMap RemoteComponent::HeaderInformation()
  46. {
  47. QVariantMap gdmInfo = GDMInformation();
  48. QVariantMap headerInfo;
  49. for(const QString& key : gdmInfo.keys())
  50. {
  51. if (g_headerKeyMap.contains(key))
  52. headerInfo[g_headerKeyMap[key]] = gdmInfo[key];
  53. }
  54. headerInfo["X-Plex-Platform"] = QSysInfo::productType();
  55. headerInfo["X-Plex-Platform-Version"] = QSysInfo::productVersion();
  56. return headerInfo;
  57. }
  58. /////////////////////////////////////////////////////////////////////////////////////////
  59. QVariantMap RemoteComponent::ResourceInformation()
  60. {
  61. QVariantMap gdmInfo = GDMInformation();
  62. QVariantMap resourceInfo;
  63. for(const QString& key : gdmInfo.keys())
  64. {
  65. if (g_resourceKeyMap.contains(key))
  66. resourceInfo[g_resourceKeyMap[key]] = gdmInfo[key];
  67. }
  68. resourceInfo["platform"] = QSysInfo::productType();
  69. resourceInfo["platformVersion"] = QSysInfo::productVersion();
  70. return resourceInfo;
  71. };
  72. /////////////////////////////////////////////////////////////////////////////////////////
  73. QVariantMap RemoteComponent::GDMInformation()
  74. {
  75. QVariantMap headers = {
  76. {"Name", Utils::ComputerName()},
  77. {"Port", SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "webserverport")},
  78. {"Version", Version::GetVersionString()},
  79. {"Product", "Plex Media Player"},
  80. {"Protocol", "plex"},
  81. {"Protocol-Version", "1"},
  82. {"Protocol-Capabilities", "navigation,playback,timeline,mirror,playqueues"},
  83. {"Device-Class", "pc"},
  84. {"Resource-Identifier", SettingsComponent::Get().value(SETTINGS_SECTION_WEBCLIENT, "clientID")}
  85. };
  86. return headers;
  87. }
  88. /////////////////////////////////////////////////////////////////////////////////////////
  89. void RemoteComponent::handleResource(QHttpRequest* request, QHttpResponse* response)
  90. {
  91. if (request->method() == qhttp::EHTTP_GET)
  92. {
  93. QVariantMap headers = ResourceInformation();
  94. QByteArray outputData;
  95. QXmlStreamWriter output(&outputData);
  96. output.setAutoFormatting(true);
  97. output.writeStartDocument();
  98. output.writeStartElement("MediaContainer");
  99. output.writeStartElement("Player");
  100. for(const QString& key : headers.keys())
  101. output.writeAttribute(key, headers[key].toString());
  102. output.writeEndElement();
  103. output.writeEndDocument();
  104. response->setStatusCode(qhttp::ESTATUS_OK);
  105. response->write(outputData);
  106. response->end();
  107. }
  108. else
  109. {
  110. response->setStatusCode(qhttp::ESTATUS_METHOD_NOT_ALLOWED);
  111. response->end();
  112. }
  113. }
  114. /////////////////////////////////////////////////////////////////////////////////////////
  115. QVariantMap RemoteComponent::QueryToMap(const QUrl& url)
  116. {
  117. QUrlQuery query(url);
  118. QVariantMap queryMap;
  119. for(auto stringPair : query.queryItems())
  120. {
  121. QString key = stringPair.first;
  122. QString value = stringPair.second;
  123. QVariantList l;
  124. if (queryMap.contains(key))
  125. {
  126. l = queryMap[key].toList();
  127. l.append(value);
  128. }
  129. else
  130. {
  131. l.append(value);
  132. }
  133. queryMap[key] = l;
  134. }
  135. return queryMap;
  136. }
  137. /////////////////////////////////////////////////////////////////////////////////////////
  138. QVariantMap RemoteComponent::HeaderToMap(const qhttp::THeaderHash& hash)
  139. {
  140. QVariantMap variantMap;
  141. for(const QString& key : hash.keys())
  142. variantMap.insert(key, hash.value(key.toUtf8()));
  143. return variantMap;
  144. }
  145. /////////////////////////////////////////////////////////////////////////////////////////
  146. void RemoteComponent::handleCommand(QHttpRequest* request, QHttpResponse* response)
  147. {
  148. QVariantMap queryMap = QueryToMap(request->url());
  149. QVariantMap headerMap = HeaderToMap(request->headers());
  150. QString identifier = headerMap["x-plex-client-identifier"].toString();
  151. response->addHeader("Access-Control-Allow-Origin", "*");
  152. response->addHeader("X-Plex-Client-Identifier", SettingsComponent::Get().value(SETTINGS_SECTION_WEBCLIENT, "clientID").toByteArray());
  153. // handle CORS requests here
  154. if ((request->method() == qhttp::EHTTP_OPTIONS) && headerMap.contains("access-control-request-method"))
  155. {
  156. response->addHeader("Content-Type", "text/plain");
  157. response->addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, HEAD");
  158. response->addHeader("Access-Control-Max-Age", "1209600");
  159. response->addHeader("Connection", "close");
  160. if (headerMap.contains("access-control-request-headers"))
  161. {
  162. response->addHeader("Access-Control-Allow-Headers", headerMap.value("access-control-request-headers").toByteArray());
  163. }
  164. response->setStatusCode(qhttp::ESTATUS_OK);
  165. response->end();
  166. return;
  167. }
  168. // we want to handle the subscription events in the host
  169. // since we are going to handle the updating later.
  170. //
  171. if (request->url().path() == "/player/timeline/subscribe")
  172. {
  173. handleSubscription(request, response, false);
  174. return;
  175. }
  176. else if (request->url().path() == "/player/timeline/unsubscribe")
  177. {
  178. subscriberRemove(request->headers()["x-plex-client-identifier"]);
  179. response->setStatusCode(qhttp::ESTATUS_OK);
  180. response->end();
  181. return;
  182. }
  183. else if ((request->url().path() == "/player/timeline/poll"))
  184. {
  185. if (!m_subscriberMap.contains(identifier))
  186. handleSubscription(request, response, true);
  187. RemotePollSubscriber *subscriber = (RemotePollSubscriber *)m_subscriberMap[identifier];
  188. if (subscriber)
  189. {
  190. subscriber->reSubscribe();
  191. subscriber->setHTTPResponse(response);
  192. // if we don't have to wait, just ship the update right away
  193. // otherwise, this will wait until next update
  194. if (! (queryMap.contains("wait") && (queryMap["wait"].toList()[0].toInt() == 1)))
  195. {
  196. subscriber->sendUpdate();
  197. }
  198. }
  199. return;
  200. }
  201. // handle commandID
  202. if (!headerMap.contains("x-plex-client-identifier") || !queryMap.contains("commandID"))
  203. {
  204. QLOG_WARN() << "Can't find a X-Plex-Client-Identifier header";
  205. response->setStatusCode(qhttp::ESTATUS_NOT_ACCEPTABLE);
  206. response->end();
  207. return;
  208. }
  209. quint64 commandId = 0;
  210. {
  211. QMutexLocker lk(&m_responseLock);
  212. commandId = ++m_commandId;
  213. m_responseMap[commandId] = response;
  214. connect(response, &QHttpResponse::done, this, &RemoteComponent::responseDone);
  215. }
  216. {
  217. QMutexLocker lk(&m_subscriberLock);
  218. if (!m_subscriberMap.contains(identifier))
  219. {
  220. QLOG_WARN() << "Failed to lock up subscriber" << identifier;
  221. response->setStatusCode(qhttp::ESTATUS_NOT_ACCEPTABLE);
  222. response->end();
  223. return;
  224. }
  225. RemoteSubscriber* subscriber = m_subscriberMap[identifier];
  226. subscriber->setCommandId(m_commandId, queryMap["commandID"].toList()[0].toInt());
  227. }
  228. QVariantMap arg = {
  229. { "method", request->methodString() },
  230. { "headers", headerMap },
  231. { "path", request->url().path() },
  232. { "query", queryMap },
  233. { "commandID", m_commandId}
  234. };
  235. emit commandReceived(arg);
  236. }
  237. /////////////////////////////////////////////////////////////////////////////////////////
  238. void RemoteComponent::responseDone()
  239. {
  240. QHttpResponse* response = dynamic_cast<QHttpResponse*>(sender());
  241. if (response)
  242. {
  243. QMutexLocker lk(&m_responseLock);
  244. int foundId = -1;
  245. for(int responseId : m_responseMap.keys())
  246. {
  247. if (m_responseMap[responseId] == response)
  248. {
  249. foundId = responseId;
  250. break;
  251. }
  252. }
  253. if (foundId != -1)
  254. m_responseMap.remove(foundId);
  255. }
  256. }
  257. /////////////////////////////////////////////////////////////////////////////////////////
  258. void RemoteComponent::commandResponse(const QVariantMap& responseArguments)
  259. {
  260. // check for minimum requirements in the responseArguments
  261. if (!responseArguments.contains("commandID") ||
  262. !responseArguments.contains("responseCode"))
  263. {
  264. QLOG_WARN() << "responseArguments did not contain a commandId or responseCode";
  265. return;
  266. }
  267. quint64 commandId = responseArguments["commandID"].toULongLong();
  268. uint responseCode = responseArguments["responseCode"].toUInt();
  269. QMutexLocker lk(&m_responseLock);
  270. if (!m_responseMap.contains(commandId))
  271. {
  272. QLOG_WARN() << "Could not find responseId:" << commandId << " - maybe it was removed because of a timeout?";
  273. return;
  274. }
  275. QHttpResponse* response = m_responseMap[commandId];
  276. // no need to hold the lock when we have changed m_responseMap
  277. lk.unlock();
  278. // add headers if we have them
  279. if (responseArguments.contains("headers") && responseArguments["headers"].type() == QVariant::Map)
  280. {
  281. QVariantMap headers = responseArguments["headers"].toMap();
  282. for(const QString& key : headers.keys())
  283. response->addHeader(key.toUtf8(), headers[key].toByteArray());
  284. }
  285. // write the response HTTP code
  286. response->setStatusCode((qhttp::TStatusCode)responseCode);
  287. // handle optional body argument
  288. if (responseArguments.contains("body") && responseArguments["body"].type() == QVariant::String)
  289. response->write(responseArguments["body"].toString().toUtf8());
  290. response->end();
  291. }
  292. /////////////////////////////////////////////////////////////////////////////////////////
  293. void RemoteComponent::handleSubscription(QHttpRequest* request, QHttpResponse* response, bool poll)
  294. {
  295. QVariantMap headers = HeaderToMap(request->headers());
  296. // check for required headers
  297. if (!headers.contains("x-plex-client-identifier") ||
  298. !headers.contains("x-plex-device-name"))
  299. {
  300. QLOG_ERROR() << "Missing X-Plex headers in /timeline/subscribe request";
  301. response->setStatusCode(qhttp::ESTATUS_BAD_REQUEST);
  302. response->end();
  303. return;
  304. }
  305. // check for required arguments
  306. QVariantMap query = QueryToMap(request->url());
  307. if (!query.contains("commandID") || ((!query.contains("port")) && !poll))
  308. {
  309. QLOG_ERROR() << "Missing arguments to /timeline/subscribe request";
  310. response->setStatusCode(qhttp::ESTATUS_BAD_REQUEST);
  311. response->end();
  312. return;
  313. }
  314. QString clientIdentifier(request->headers()["x-plex-client-identifier"]);
  315. QMutexLocker lk(&m_subscriberLock);
  316. RemoteSubscriber* subscriber = nullptr;
  317. if (m_subscriberMap.contains(clientIdentifier))
  318. {
  319. QLOG_DEBUG() << "Refreshed subscriber:" << clientIdentifier;
  320. subscriber = m_subscriberMap[clientIdentifier];
  321. subscriber->reSubscribe();
  322. }
  323. else
  324. {
  325. if (poll)
  326. {
  327. QLOG_DEBUG() << "New poll subscriber:" << clientIdentifier << request->headers()["x-plex-device-name"];
  328. subscriber = new RemotePollSubscriber(clientIdentifier, request->headers()["x-plex-device-name"], response, this);
  329. }
  330. else
  331. {
  332. QUrl address;
  333. QString protocol = query.contains("protocol") ? query["protocol"].toList()[0].toString() : "http";
  334. int port = query.contains("port") ? query["port"].toList()[0].toInt() : 32400;
  335. address.setScheme(protocol);
  336. address.setHost(request->remoteAddress());
  337. address.setPort(port);
  338. QLOG_DEBUG() << "New subscriber:" << clientIdentifier << request->headers()["x-plex-device-name"] << address.toString();
  339. subscriber = new RemoteSubscriber(clientIdentifier, request->headers()["x-plex-device-name"], address, this);
  340. }
  341. m_subscriberMap[clientIdentifier] = subscriber;
  342. // if it's our first controller, we notify web for subscription
  343. if (m_subscriberMap.size() == 1)
  344. {
  345. QLOG_DEBUG() << "First subscriber added, subscribing to web";
  346. subscribeToWeb(true);
  347. }
  348. }
  349. subscriber->setCommandId(m_commandId, query["commandID"].toList()[0].toInt());
  350. if (!poll)
  351. {
  352. subscriber->sendUpdate();
  353. response->setStatusCode(qhttp::ESTATUS_OK);
  354. response->end();
  355. }
  356. }
  357. /////////////////////////////////////////////////////////////////////////////////////////
  358. void RemoteComponent::subscribeToWeb(bool subscribe)
  359. {
  360. QVariantMap arg = {
  361. { "method", "GET" },
  362. { "path", "/player/timeline/" + (subscribe ? QLatin1String("subscribe") : QLatin1String("unsubscribe")) },
  363. { "commandID", m_commandId}
  364. };
  365. emit commandReceived(arg);
  366. }
  367. /////////////////////////////////////////////////////////////////////////////////////////
  368. void RemoteComponent::checkSubscribers()
  369. {
  370. QMutexLocker lk(&m_subscriberLock);
  371. QList<RemoteSubscriber*> subsToRemove;
  372. for(RemoteSubscriber* subscriber : m_subscriberMap.values())
  373. {
  374. // was it more than 10 seconds since this client checked in last?
  375. if (subscriber->lastSubscribe() > 90 * 1000)
  376. {
  377. QLOG_DEBUG() << "more than 10 seconds since we heard from:" << subscriber->deviceName() << "- unsubscribing..";
  378. subsToRemove << subscriber;
  379. }
  380. }
  381. lk.unlock();
  382. for(RemoteSubscriber* sub : subsToRemove)
  383. subscriberRemove(sub->clientIdentifier());
  384. }
  385. /////////////////////////////////////////////////////////////////////////////////////////
  386. QNetworkAccessManager* RemoteComponent::getNetworkAccessManager()
  387. {
  388. // we might want to set common options here.
  389. return m_networkAccessManager;
  390. }
  391. /////////////////////////////////////////////////////////////////////////////////////////
  392. void RemoteComponent::timelineFinished(QNetworkReply* reply)
  393. {
  394. QString identifier = reply->request().attribute(QNetworkRequest::User).toString();
  395. // ignore requests with no identifier
  396. if (identifier.isEmpty())
  397. return;
  398. QMutexLocker lk(&m_subscriberLock);
  399. if (!m_subscriberMap.contains(identifier))
  400. {
  401. QLOG_WARN() << "Got a networkreply with a identifier we don't know about:" << identifier;
  402. return;
  403. }
  404. RemoteSubscriber* sub = m_subscriberMap[identifier];
  405. lk.unlock();
  406. sub->timelineFinished(reply);
  407. }
  408. /////////////////////////////////////////////////////////////////////////////////////////
  409. void RemoteComponent::subscriberRemove(const QString& identifier)
  410. {
  411. QMutexLocker lk(&m_subscriberLock);
  412. if (!m_subscriberMap.contains(identifier))
  413. {
  414. QLOG_ERROR() << "Can't remove client:" << identifier << "since we don't know about it.";
  415. return;
  416. }
  417. RemoteSubscriber* subscriber = m_subscriberMap[identifier];
  418. m_subscriberMap.remove(identifier);
  419. subscriber->deleteLater();
  420. // if it's our first controller, we notify web for subscription
  421. if (m_subscriberMap.size() == 0)
  422. {
  423. QLOG_DEBUG() << "Last subscriber removed, unsubscribing from web";
  424. subscribeToWeb(false);
  425. }
  426. QLOG_DEBUG() << "Removed subscriber:" << identifier;
  427. }
  428. /////////////////////////////////////////////////////////////////////////////////////////
  429. void RemoteComponent::timelineUpdate(quint64 commandID, const QString& timeline)
  430. {
  431. QMutexLocker lk(&m_subscriberLock);
  432. for(RemoteSubscriber* subscriber : m_subscriberMap.values())
  433. {
  434. subscriber->queueTimeline(commandID, timeline.toUtf8());
  435. subscriber->sendUpdate();
  436. }
  437. }