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