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