MythTV  master
upnpsubscription.cpp
Go to the documentation of this file.
1 /*
2 An HttpServer Extension that manages subscriptions to UPnP services.
3 
4 An object wishing to subscribe to a service needs to register as a listener for
5 events and subscribe using a valid usn and subscription path. The subscriber
6 is responsible for requesting a renewal before the subscription expires,
7 removing any stale subscriptions, unsubsubscribing on exit and must re-implement
8 QObject::customEvent to receive event notifications for subscribed services.
9 */
10 
11 #include "upnpsubscription.h"
12 
13 #if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
14 #include <QStringConverter>
15 #else
16 #include <QTextCodec>
17 #endif
18 #include <utility>
19 
22 #include "libmythbase/mythtypes.h"
23 
24 #include "bufferedsocketdevice.h"
25 
26 // default requested time for subscription (actual is dictated by server)
27 static constexpr uint16_t SUBSCRIPTION_TIME { 1800 };
28 // maximum time to wait for responses to subscription requests (UPnP spec. 30s)
29 static constexpr std::chrono::milliseconds MAX_WAIT { 30s };
30 
31 #define LOC QString("UPnPSub: ")
32 
34 {
35  public:
36  Subscription(QUrl url, QString path)
37  : m_url(std::move(url)), m_path(std::move(path)) { }
38  QUrl m_url;
39  QString m_path;
40  QString m_uuid;
41 };
42 
43 UPNPSubscription::UPNPSubscription(const QString &share_path, int port)
44  : HttpServerExtension("UPnPSubscriptionManager", share_path)
45 {
46  m_nSupportedMethods = (uint)RequestTypeNotify; // Only NOTIFY supported
47 
48  QHostAddress addr;
49  if (!UPnp::g_IPAddrList.isEmpty())
50  addr = UPnp::g_IPAddrList.at(0);
51 
52  QString host;
53  // taken from MythCoreContext
54  if (addr.protocol() == QAbstractSocket::IPv6Protocol)
55  host = "[" + addr.toString() + "]";
56  else
57  host = addr.toString();
58 
59  m_callback = QString("http://%1:%2/Subscriptions/event?usn=")
60  .arg(host, QString::number(port));
61 }
62 
64 {
65  m_subscriptionLock.lock();
66  QList<QString> usns = m_subscriptions.keys();
67  while (!usns.isEmpty())
68  Unsubscribe(usns.takeLast());
69  m_subscriptions.clear();
70  m_subscriptionLock.unlock();
71 
72  LOG(VB_UPNP, LOG_DEBUG, LOC + "Finished");
73 }
74 
75 std::chrono::seconds UPNPSubscription::Subscribe(const QString &usn, const QUrl &url,
76  const QString &path)
77 {
78  LOG(VB_UPNP, LOG_DEBUG, LOC + QString("Subscribe %1 %2 %3")
79  .arg(usn, url.toString(), path));
80 
81  // N.B. this is called from the client object's thread. Hence we have to
82  // lock until the subscription request has returned, otherwise we may
83  // receive the first event notification (in the HttpServer thread)
84  // before the subscription is processed and the event will fail
85 
86  QMutexLocker lock(&m_subscriptionLock);
87  if (m_subscriptions.contains(usn))
88  {
89  if (m_subscriptions[usn]->m_url != url ||
90  m_subscriptions[usn]->m_path != path)
91  {
92  LOG(VB_GENERAL, LOG_WARNING, LOC +
93  "Re-subscribing with different url and path.");
94  m_subscriptions[usn]->m_url = url;
95  m_subscriptions[usn]->m_path = path;
96  m_subscriptions[usn]->m_uuid = QString();
97  }
98  }
99  else
100  {
101  m_subscriptions.insert(usn, new Subscription(url, path));
102  }
103 
104  return SendSubscribeRequest(m_callback, usn, url, path, QString(),
105  m_subscriptions[usn]->m_uuid);
106 }
107 
108 void UPNPSubscription::Unsubscribe(const QString &usn)
109 {
110  QUrl url;
111  QString path;
112  QString uuid = QString();
113  m_subscriptionLock.lock();
114  if (m_subscriptions.contains(usn))
115  {
116  url = m_subscriptions[usn]->m_url;
117  path = m_subscriptions[usn]->m_path;
118  uuid = m_subscriptions[usn]->m_uuid;
119  delete m_subscriptions.value(usn);
120  m_subscriptions.remove(usn);
121  }
122  m_subscriptionLock.unlock();
123 
124  if (!uuid.isEmpty())
125  SendUnsubscribeRequest(usn, url, path, uuid);
126 }
127 
128 std::chrono::seconds UPNPSubscription::Renew(const QString &usn)
129 {
130  LOG(VB_UPNP, LOG_DEBUG, LOC + QString("Renew: %1").arg(usn));
131 
132  QUrl url;
133  QString path;
134  QString sid;
135 
136  // see locking comment in Subscribe
137  QMutexLocker lock(&m_subscriptionLock);
138  if (m_subscriptions.contains(usn))
139  {
140  url = m_subscriptions[usn]->m_url;
141  path = m_subscriptions[usn]->m_path;
142  sid = m_subscriptions[usn]->m_uuid;
143  }
144  else
145  {
146  LOG(VB_UPNP, LOG_ERR, LOC + QString("Unrecognised renewal usn: %1")
147  .arg(usn));
148  return 0s;
149  }
150 
151  if (!sid.isEmpty())
152  {
153  return SendSubscribeRequest(m_callback, usn, url, path, sid,
154  m_subscriptions[usn]->m_uuid);
155  }
156 
157  LOG(VB_UPNP, LOG_ERR, LOC + QString("No uuid - not renewing usn: %1")
158  .arg(usn));
159  return 0s;
160 }
161 
162 void UPNPSubscription::Remove(const QString &usn)
163 {
164  // this could be handled by hooking directly into the SSDPCache updates
165  // but the subscribing object will also be doing so. Having the that
166  // object initiate the removal avoids temoporary race conditions during
167  // periods of UPnP/SSDP activity
168  m_subscriptionLock.lock();
169  if (m_subscriptions.contains(usn))
170  {
171  LOG(VB_UPNP, LOG_INFO, LOC + QString("Removing %1").arg(usn));
172  delete m_subscriptions.value(usn);
173  m_subscriptions.remove(usn);
174  }
175  m_subscriptionLock.unlock();
176 }
177 
179 {
180  if (!pRequest)
181  return false;
182 
183  if (pRequest->m_sBaseUrl != "/Subscriptions")
184  return false;
185  if (pRequest->m_sMethod != "event")
186  return false;
187 
188  LOG(VB_UPNP, LOG_DEBUG, LOC + QString("%1\n%2")
189  .arg(pRequest->m_sRawRequest, pRequest->m_sPayload));
190 
191  if (pRequest->m_sPayload.isEmpty())
192  return true;
193 
194  pRequest->m_eResponseType = ResponseTypeHTML;
195 
196  QString nt = pRequest->GetLastHeader("nt");
197  QString nts = pRequest->GetLastHeader("nts");
198  bool no = (pRequest->m_eType == RequestTypeNotify);
199 
200  if (nt.isEmpty() || nts.isEmpty() || !no)
201  {
202  pRequest->m_nResponseStatus = 400;
203  return true;
204  }
205 
206  pRequest->m_nResponseStatus = 412;
207  if (nt != "upnp:event" || nts != "upnp:propchange")
208  return true;
209 
210  QString usn = pRequest->m_mapParams["usn"];
211  QString sid = pRequest->GetLastHeader("sid");
212  if (usn.isEmpty() || sid.isEmpty())
213  return true;
214 
215  // N.B. Validating the usn and uuid here might mean blocking for some time
216  // while waiting for a subscription to complete. While this operates in a
217  // worker thread, worker threads are a limited resource which we could
218  // rapidly overload if a number of events arrive. Instead let the
219  // subscribing objects validate the usn - the uuid should be superfluous.
220 
221  QString seq = pRequest->GetLastHeader("seq");
222 
223  // mediatomb sends some extra character(s) at the end of the payload
224  // which throw Qt, so try and trim them off
225  int loc = pRequest->m_sPayload.lastIndexOf("propertyset>");
226  QString payload = (loc > -1) ? pRequest->m_sPayload.left(loc + 12) :
227  pRequest->m_sPayload;
228 
229  LOG(VB_UPNP, LOG_DEBUG, LOC + QString("Payload:\n%1").arg(payload));
230 
231  pRequest->m_nResponseStatus = 400;
232  QDomDocument body;
233 #if QT_VERSION < QT_VERSION_CHECK(6,5,0)
234  QString error;
235  int errorCol = 0;
236  int errorLine = 0;
237  if (!body.setContent(payload, true, &error, &errorLine, &errorCol))
238  {
239  LOG(VB_GENERAL, LOG_ERR, LOC +
240  QString("Failed to parse event: Line: %1 Col: %2 Error: '%3'")
241  .arg(errorLine).arg(errorCol).arg(error));
242  return true;
243  }
244 #else
245  auto parseResult =
246  body.setContent(payload,
247  QDomDocument::ParseOption::UseNamespaceProcessing);
248  if (!parseResult)
249  {
250  LOG(VB_GENERAL, LOG_ERR, LOC +
251  QString("Failed to parse event: Line: %1 Col: %2 Error: '%3'")
252  .arg(parseResult.errorLine).arg(parseResult.errorColumn)
253  .arg(parseResult.errorMessage));
254  return true;
255  }
256 #endif
257 
258  LOG(VB_UPNP, LOG_DEBUG, LOC + "/n/n" + body.toString(4) + "/n/n");
259 
260  QDomNodeList properties = body.elementsByTagName("property");
262 
263  // this deals with both one argument per property (compliant) and mutliple
264  // arguments per property as sent by mediatomb
265  for (int i = 0; i < properties.size(); i++)
266  {
267  QDomNodeList arguments = properties.at(i).childNodes();
268  for (int j = 0; j < arguments.size(); j++)
269  {
270  QDomElement e = arguments.at(j).toElement();
271  if (!e.isNull() && !e.text().isEmpty() && !e.tagName().isEmpty())
272  results.insert(e.tagName(), e.text());
273  }
274  }
275 
276  // using MythObservable allows multiple objects to subscribe to the same
277  // service but is less efficient from an eventing perspective, especially
278  // if multiple objects are subscribing
279  if (!results.isEmpty())
280  {
281  pRequest->m_nResponseStatus = 200;
282  results.insert("usn", usn);
283  results.insert("seq", seq);
284  MythInfoMapEvent me("UPNP_EVENT", results);
285  dispatch(me);
286  }
287 
288  return true;
289 }
290 
292  const QUrl &url,
293  const QString &path,
294  const QString &uuid)
295 {
296  bool success = false;
297  QString host = url.host();
298  int port = url.port();
299 
300  QByteArray sub;
301  QTextStream data(&sub);
302 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
303  data.setCodec(QTextCodec::codecForName("UTF-8"));
304 #else
305  data.setEncoding(QStringConverter::Utf8);
306 #endif
307  // N.B. Play On needs an extra space between UNSUBSCRIBE and path...
308  data << QString("UNSUBSCRIBE %1 HTTP/1.1\r\n").arg(path);
309  data << QString("HOST: %1:%2\r\n").arg(host, QString::number(port));
310  data << QString("SID: uuid:%1\r\n").arg(uuid);
311  data << "\r\n";
312  data.flush();
313 
314  LOG(VB_UPNP, LOG_DEBUG, LOC + "\n\n" + sub);
315 
316  auto *sockdev = new MSocketDevice(MSocketDevice::Stream);
317  auto *sock = new BufferedSocketDevice(sockdev);
318  sockdev->setBlocking(true);
319 
320  if (sock->Connect(QHostAddress(host), port))
321  {
322  if (sock->WriteBlockDirect(sub.constData(), sub.size()) != -1)
323  {
324  QString line = sock->ReadLine(MAX_WAIT);
325  success = !line.isEmpty();
326  }
327  else
328  {
329  LOG(VB_GENERAL, LOG_ERR, LOC +
330  QString("Socket write error for %1:%2") .arg(host).arg(port));
331  }
332  sock->Close();
333  }
334  else
335  {
336  LOG(VB_GENERAL, LOG_ERR, LOC +
337  QString("Failed to open socket for %1:%2") .arg(host).arg(port));
338  }
339 
340  delete sock;
341  delete sockdev;
342  if (success)
343  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Unsubscribed to %1").arg(usn));
344  else
345  LOG(VB_UPNP, LOG_WARNING, LOC + QString("Failed to unsubscribe to %1")
346  .arg(usn));
347  return success;
348 }
349 
350 std::chrono::seconds UPNPSubscription::SendSubscribeRequest(const QString &callback,
351  const QString &usn,
352  const QUrl &url,
353  const QString &path,
354  const QString &uuidin,
355  QString &uuidout)
356 {
357  QString host = url.host();
358  int port = url.port();
359 
360  QByteArray sub;
361  QTextStream data(&sub);
362 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
363  data.setCodec(QTextCodec::codecForName("UTF-8"));
364 #else
365  data.setEncoding(QStringConverter::Utf8);
366 #endif
367  // N.B. Play On needs an extra space between SUBSCRIBE and path...
368  data << QString("SUBSCRIBE %1 HTTP/1.1\r\n").arg(path);
369  data << QString("HOST: %1:%2\r\n").arg(host, QString::number(port));
370 
371 
372  if (uuidin.isEmpty()) // new subscription
373  {
374  data << QString("CALLBACK: <%1%2>\r\n")
375  .arg(callback, usn);
376  data << "NT: upnp:event\r\n";
377  }
378  else
379  {
380  // renewal
381  data << QString("SID: uuid:%1\r\n").arg(uuidin);
382  }
383 
384  data << QString("TIMEOUT: Second-%1\r\n").arg(SUBSCRIPTION_TIME);
385  data << "\r\n";
386  data.flush();
387 
388  LOG(VB_UPNP, LOG_DEBUG, LOC + "\n\n" + sub);
389 
390  auto *sockdev = new MSocketDevice(MSocketDevice::Stream);
391  auto *sock = new BufferedSocketDevice(sockdev);
392  sockdev->setBlocking(true);
393 
394  QString uuid;
395  QString timeout;
396  std::chrono::seconds result = 0s;
397 
398  if (sock->Connect(QHostAddress(host), port))
399  {
400  if (sock->WriteBlockDirect(sub.constData(), sub.size()) != -1)
401  {
402  bool ok = false;
403  QString line = sock->ReadLine(MAX_WAIT);
404  while (!line.isEmpty())
405  {
406  LOG(VB_UPNP, LOG_DEBUG, LOC + line);
407  if (line.contains("HTTP/1.1 200 OK", Qt::CaseInsensitive))
408  ok = true;
409  if (line.startsWith("SID:", Qt::CaseInsensitive))
410  uuid = line.mid(4).trimmed().mid(5).trimmed();
411  if (line.startsWith("TIMEOUT:", Qt::CaseInsensitive))
412  timeout = line.mid(8).trimmed().mid(7).trimmed();
413  if (ok && !uuid.isEmpty() && !timeout.isEmpty())
414  break;
415  line = sock->ReadLine(MAX_WAIT);
416  }
417 
418  if (ok && !uuid.isEmpty() && !timeout.isEmpty())
419  {
420  uuidout = uuid;
421  result = std::chrono::seconds(timeout.toUInt());
422  }
423  else
424  {
425  LOG(VB_GENERAL, LOG_ERR, LOC +
426  QString("Failed to subscribe to %1").arg(usn));
427  }
428  }
429  else
430  {
431  LOG(VB_GENERAL, LOG_ERR, LOC +
432  QString("Socket write error for %1:%2") .arg(host).arg(port));
433  }
434  sock->Close();
435  }
436  else
437  {
438  LOG(VB_GENERAL, LOG_ERR, LOC +
439  QString("Failed to open socket for %1:%2") .arg(host).arg(port));
440  }
441 
442  delete sock;
443  delete sockdev;
444  return result;
445 }
UPNPSubscription::Renew
std::chrono::seconds Renew(const QString &usn)
Definition: upnpsubscription.cpp:128
HTTPRequest::m_sBaseUrl
QString m_sBaseUrl
Definition: httprequest.h:127
SUBSCRIPTION_TIME
static constexpr uint16_t SUBSCRIPTION_TIME
Definition: upnpsubscription.cpp:27
HttpServerExtension::m_nSupportedMethods
uint m_nSupportedMethods
Definition: httpserver.h:83
HTTPRequest
Definition: httprequest.h:109
hardwareprofile.smolt.timeout
float timeout
Definition: smolt.py:102
Subscription::m_path
QString m_path
Definition: upnpsubscription.cpp:39
BufferedSocketDevice
Definition: bufferedsocketdevice.h:38
error
static void error(const char *str,...)
Definition: vbi.cpp:37
upnpsubscription.h
Subscription::m_url
QUrl m_url
Definition: upnpsubscription.cpp:38
HTTPRequest::m_sMethod
QString m_sMethod
Definition: httprequest.h:129
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
UPNPSubscription::SendSubscribeRequest
static std::chrono::seconds SendSubscribeRequest(const QString &callback, const QString &usn, const QUrl &url, const QString &path, const QString &uuidin, QString &uuidout)
Definition: upnpsubscription.cpp:350
MythObservable::dispatch
void dispatch(const MythEvent &event)
Dispatch an event to all listeners.
Definition: mythobservable.cpp:73
UPNPSubscription::Unsubscribe
void Unsubscribe(const QString &usn)
Definition: upnpsubscription.cpp:108
InfoMap
QHash< QString, QString > InfoMap
Definition: mythtypes.h:15
hardwareprofile.os_detect.results
results
Definition: os_detect.py:295
UPNPSubscription::Subscribe
std::chrono::seconds Subscribe(const QString &usn, const QUrl &url, const QString &path)
Definition: upnpsubscription.cpp:75
UPNPSubscription::m_subscriptionLock
QRecursiveMutex m_subscriptionLock
Definition: upnpsubscription.h:35
HTTPRequest::m_nResponseStatus
long m_nResponseStatus
Definition: httprequest.h:152
mythlogging.h
HTTPRequest::m_mapParams
QStringMap m_mapParams
Definition: httprequest.h:131
mythtypes.h
Subscription::Subscription
Subscription(QUrl url, QString path)
Definition: upnpsubscription.cpp:36
UPNPSubscription::m_subscriptions
QHash< QString, Subscription * > m_subscriptions
Definition: upnpsubscription.h:34
bufferedsocketdevice.h
RequestTypeNotify
@ RequestTypeNotify
Definition: httprequest.h:60
HTTPRequest::m_sRawRequest
QString m_sRawRequest
Definition: httprequest.h:123
ResponseTypeHTML
@ ResponseTypeHTML
Definition: httprequest.h:79
Subscription
Definition: upnpsubscription.cpp:33
MythInfoMapEvent
Definition: mythevent.h:128
Subscription::m_uuid
QString m_uuid
Definition: upnpsubscription.cpp:40
HTTPRequest::GetLastHeader
QString GetLastHeader(const QString &sType) const
Definition: httprequest.cpp:154
mythcorecontext.h
HTTPRequest::m_eResponseType
HttpResponseType m_eResponseType
Definition: httprequest.h:149
HTTPRequest::m_sPayload
QString m_sPayload
Definition: httprequest.h:135
LOC
#define LOC
Definition: upnpsubscription.cpp:31
UPNPSubscription::ProcessRequest
bool ProcessRequest(HTTPRequest *pRequest) override
Definition: upnpsubscription.cpp:178
MAX_WAIT
static constexpr std::chrono::milliseconds MAX_WAIT
Definition: upnpsubscription.cpp:29
UPNPSubscription::SendUnsubscribeRequest
static bool SendUnsubscribeRequest(const QString &usn, const QUrl &url, const QString &path, const QString &uuid)
Definition: upnpsubscription.cpp:291
uint16_t
unsigned short uint16_t
Definition: iso6937tables.h:3
UPNPSubscription::~UPNPSubscription
~UPNPSubscription() override
Definition: upnpsubscription.cpp:63
HttpServerExtension
Definition: httpserver.h:71
UPNPSubscription::UPNPSubscription
UPNPSubscription(const QString &share_path, int port)
Definition: upnpsubscription.cpp:43
UPnp::g_IPAddrList
static QList< QHostAddress > g_IPAddrList
Definition: upnp.h:110
UPNPSubscription::Remove
void Remove(const QString &usn)
Definition: upnpsubscription.cpp:162
uint
unsigned int uint
Definition: freesurround.h:24
HTTPRequest::m_eType
HttpRequestType m_eType
Definition: httprequest.h:120
UPNPSubscription::m_callback
QString m_callback
Definition: upnpsubscription.h:36