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