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