MythTV master
ssdp.cpp
Go to the documentation of this file.
1
2// Program Name: ssdp.cpp
3// Created : Oct. 1, 2005
4//
5// Purpose : SSDP Discovery Service Implmenetation
6//
7// Copyright (c) 2005 David Blain <dblain@mythtv.org>
8//
9// Licensed under the GPL v2 or later, see LICENSE for details
10//
12#include "ssdp.h"
13
14#include <algorithm>
15#include <chrono> // for milliseconds
16#include <thread> // for sleep_for
17
18#include <QByteArray>
19#include <QHostAddress>
20#include <QMap>
21#include <QMutex>
22#include <QMutexLocker>
23#include <QNetworkDatagram>
24#include <QRegularExpression>
25#include <QString>
26#include <QStringList>
27#include <QUdpSocket>
28
33
34#include "ssdpcache.h"
35#include "taskqueue.h"
36#include "upnp.h"
37#include "upnptasknotify.h"
38#include "upnptasksearch.h"
39
42//
43// SSDP Class Implementation
44//
47
48// We're creating this class immediately so it will always be available.
49
50static QMutex g_pSSDPCreationLock;
51SSDP* SSDP::g_pSSDP = nullptr;
52
54//
56
58{
59 QMutexLocker locker(&g_pSSDPCreationLock);
60 return g_pSSDP ? g_pSSDP : (g_pSSDP = new SSDP());
61}
62
64//
66
68{
69 QMutexLocker locker(&g_pSSDPCreationLock);
70 delete g_pSSDP;
71 g_pSSDP = nullptr;
72}
73
75//
77
79{
80 m_receiver.moveToThread(m_thread.qthread());
82 LOG(VB_UPNP, LOG_NOTICE, "SSDP instance created." );
83}
84
86//
88
90{
91 LOG(VB_UPNP, LOG_NOTICE, "Destroying SSDP instance." );
92
94 if (m_pNotifyTask != nullptr)
95 {
97 m_pNotifyTask = nullptr;
98 }
100 m_thread.quit();
101 m_thread.wait();
102
103 LOG(VB_UPNP, LOG_INFO, "SSDP instance destroyed." );
104}
105
107//
109
110void SSDP::EnableNotifications( int nServicePort )
111{
112 if ( m_pNotifyTask == nullptr )
113 {
114 m_nServicePort = nServicePort;
115
116 LOG(VB_UPNP, LOG_INFO,
117 "SSDP::EnableNotifications() - creating new task");
119
120 // ------------------------------------------------------------------
121 // First Send out Notification that we are leaving the network.
122 // ------------------------------------------------------------------
123
124 LOG(VB_UPNP, LOG_INFO,
125 "SSDP::EnableNotifications() - sending NTS_byebye");
127 m_pNotifyTask->Execute( nullptr );
128 }
129
130 // ------------------------------------------------------------------
131 // Add Announcement Task to the Queue
132 // ------------------------------------------------------------------
133
134 LOG(VB_UPNP, LOG_INFO, "SSDP::EnableNotifications() - sending NTS_alive");
135
137
139
140 LOG(VB_UPNP, LOG_INFO,
141 "SSDP::EnableNotifications() - Task added to UPnP queue");
142}
143
145//
147
149{
150 if (m_pNotifyTask != nullptr)
151 {
152 // Send Announcement that we are leaving.
153
155 m_pNotifyTask->Execute( nullptr );
156 }
157}
158
159void SSDP::PerformSearch(const QString &sST, std::chrono::seconds timeout)
160{
162}
163
164void SSDPReceiver::performSearch(const QString &sST, std::chrono::seconds timeout)
165{
166 timeout = std::clamp(timeout, 1s, 5s);
167 QString rRequest = QString("M-SEARCH * HTTP/1.1\r\n"
168 "HOST: 239.255.255.250:1900\r\n"
169 "MAN: \"ssdp:discover\"\r\n"
170 "MX: %1\r\n"
171 "ST: %2\r\n"
172 "\r\n")
173 .arg(timeout.count()).arg(sST);
174
175 LOG(VB_UPNP, LOG_DEBUG, QString("Sending SSDP search datagram\n%1").arg(rRequest));
176
177 QByteArray sRequest = rRequest.toUtf8();
178 int nSize = sRequest.size();
179
180 if (m_socket.writeDatagram(sRequest, QHostAddress(QString(SSDP_GROUP)), SSDP_PORT) != nSize)
181 {
182 LOG(VB_GENERAL, LOG_INFO, "SSDP::PerformSearch - did not write entire buffer.");
183 }
184
185 std::this_thread::sleep_for(std::chrono::milliseconds(MythRandom(0, 250)));
186
187 if (m_socket.writeDatagram(sRequest, QHostAddress(QString(SSDP_GROUP)), SSDP_PORT) != nSize)
188 {
189 LOG(VB_GENERAL, LOG_INFO, "SSDP::PerformSearch - did not write entire buffer.");
190 }
191}
192
193static SSDPRequestType ProcessRequestLine(const QString &sLine)
194{
195 static const QRegularExpression k_whitespace {"\\s+"};
196 QStringList tokens = sLine.split(k_whitespace, Qt::SkipEmptyParts);
197
198 // ----------------------------------------------------------------------
199 // if this is actually a response, then sLine's format will be:
200 // HTTP/m.n <response code> <response text>
201 // otherwise:
202 // <method> <Resource URI> HTTP/m.n
203 // ----------------------------------------------------------------------
204
205 if ( sLine.startsWith( QString("HTTP/") ))
206 return SSDP_MSearchResp;
207
208 if (tokens.count() > 0)
209 {
210 if (tokens[0] == "M-SEARCH" ) return SSDP_MSearch;
211 if (tokens[0] == "NOTIFY" ) return SSDP_Notify;
212 }
213
214 return SSDP_Unknown;
215}
216
217static QString GetHeaderValue(const QMap<QString, QString> &headers,
218 const QString &sKey, const QString &sDefault )
219{
220 QMap<QString, QString>::const_iterator it = headers.find(sKey.toLower());
221
222 if ( it == headers.end())
223 return sDefault;
224
225 return *it;
226}
227
228static bool ProcessSearchRequest(const QMap<QString, QString> &sHeaders,
229 const QHostAddress& peerAddress,
230 quint16 peerPort,
231 int servicePort)
232{
233 // Don't respond if notifications are not enabled.
234 if (servicePort == 0)
235 {
236 return true;
237 }
238
239 QString sMAN = GetHeaderValue( sHeaders, "MAN", "" );
240 QString sST = GetHeaderValue( sHeaders, "ST" , "" );
241 QString sMX = GetHeaderValue( sHeaders, "MX" , "" );
242 std::chrono::seconds nMX = 0s;
243
244 LOG(VB_UPNP, LOG_DEBUG, QString("SSDP::ProcessSearchrequest : [%1] MX=%2")
245 .arg(sST, sMX));
246
247 // ----------------------------------------------------------------------
248 // Validate Header Values...
249 // ----------------------------------------------------------------------
250
251#if 0
252 if ( pRequest->m_sMethod != "*" ) return false;
253 if ( pRequest->m_sProtocol != "HTTP" ) return false;
254 if ( pRequest->m_nMajor != 1 ) return false;
255#endif
256 if ( sMAN != "\"ssdp:discover\"" ) return false;
257 if ( sST.length() == 0 ) return false;
258 if ( sMX.length() == 0 ) return false;
259 nMX = std::chrono::seconds(sMX.toInt());
260 if ( nMX <= 0s ) return false;
261
262 // ----------------------------------------------------------------------
263 // Adjust timeout to be a random interval between 0 and MX (max of 120)
264 // ----------------------------------------------------------------------
265
266 nMX = std::clamp(nMX, 0s, 120s);
267
268 auto nNewMX = std::chrono::milliseconds(MythRandom(0, (duration_cast<std::chrono::milliseconds>(nMX)).count()));
269
270 // ----------------------------------------------------------------------
271 // See what they are looking for...
272 // ----------------------------------------------------------------------
273
274 if ((sST == "ssdp:all") || (sST == "upnp:rootdevice"))
275 {
276 auto *pTask = new UPnpSearchTask(servicePort,
277 peerAddress, peerPort, sST,
278 UPnp::g_UPnpDeviceDesc.m_rootDevice.GetUDN());
279
280#if 0
281 // Excute task now for fastest response, queue for time-delayed response
282 // -=>TODO: To be trully uPnp compliant, this Execute should be removed.
283 pTask->Execute( nullptr );
284#endif
285
286 TaskQueue::Instance()->AddTask( nNewMX, pTask );
287
288 pTask->DecrRef();
289
290 return true;
291 }
292
293 // ----------------------------------------------------------------------
294 // Look for a specific device/service
295 // ----------------------------------------------------------------------
296
298 &(UPnp::g_UPnpDeviceDesc.m_rootDevice), sST );
299
300 if (sUDN.length() > 0)
301 {
302 auto *pTask = new UPnpSearchTask(servicePort, peerAddress,
303 peerPort, sST, sUDN );
304
305 // Excute task now for fastest response, queue for time-delayed response
306 // -=>TODO: To be trully uPnp compliant, this Execute should be removed.
307 pTask->Execute( nullptr );
308
309 TaskQueue::Instance()->AddTask( nNewMX, pTask );
310
311 pTask->DecrRef();
312
313 return true;
314 }
315
316 return false;
317}
318
319static bool ProcessSearchResponse(const QMap<QString, QString> &headers)
320{
321 QString sDescURL = GetHeaderValue( headers, "LOCATION" , "" );
322 QString sST = GetHeaderValue( headers, "ST" , "" );
323 QString sUSN = GetHeaderValue( headers, "USN" , "" );
324 QString sCache = GetHeaderValue( headers, "CACHE-CONTROL" , "" );
325
326 LOG(VB_UPNP, LOG_DEBUG,
327 QString( "SSDP::ProcessSearchResponse ...\n"
328 "DescURL=%1\n"
329 "ST =%2\n"
330 "USN =%3\n"
331 "Cache =%4")
332 .arg(sDescURL, sST, sUSN, sCache));
333
334 int nPos = sCache.indexOf("max-age", 0, Qt::CaseInsensitive);
335
336 if (nPos < 0)
337 return false;
338
339 nPos = sCache.indexOf("=", nPos);
340 if (nPos < 0)
341 return false;
342
343 auto nSecs = std::chrono::seconds(sCache.mid( nPos+1 ).toInt());
344
345 SSDPCache::Instance()->Add( sST, sUSN, sDescURL, nSecs );
346
347 return true;
348}
349
350static bool ProcessNotify(const QMap<QString, QString> &headers)
351{
352 QString sDescURL = GetHeaderValue( headers, "LOCATION" , "" );
353 QString sNTS = GetHeaderValue( headers, "NTS" , "" );
354 QString sNT = GetHeaderValue( headers, "NT" , "" );
355 QString sUSN = GetHeaderValue( headers, "USN" , "" );
356 QString sCache = GetHeaderValue( headers, "CACHE-CONTROL" , "" );
357
358 LOG(VB_UPNP, LOG_DEBUG,
359 QString( "SSDP::ProcessNotify ...\n"
360 "DescURL=%1\n"
361 "NTS =%2\n"
362 "NT =%3\n"
363 "USN =%4\n"
364 "Cache =%5" )
365 .arg(sDescURL, sNTS, sNT, sUSN, sCache));
366
367 if (sNTS.contains( "ssdp:alive"))
368 {
369 int nPos = sCache.indexOf("max-age", 0, Qt::CaseInsensitive);
370
371 if (nPos < 0)
372 return false;
373
374 nPos = sCache.indexOf("=", nPos);
375 if (nPos < 0)
376 return false;
377
378 auto nSecs = std::chrono::seconds(sCache.mid( nPos+1 ).toInt());
379
380 SSDPCache::Instance()->Add( sNT, sUSN, sDescURL, nSecs );
381
382 return true;
383 }
384
385
386 if ( sNTS.contains( "ssdp:byebye" ) )
387 {
388 SSDPCache::Instance()->Remove( sNT, sUSN );
389
390 return true;
391 }
392
393 return false;
394}
395
397 m_port(XmlConfiguration().GetValue("UPnP/SSDP/Port", SSDP_PORT))
398{
399 m_socket.bind(QHostAddress::AnyIPv4, m_port, QUdpSocket::ShareAddress);
400 m_socket.joinMulticastGroup(m_groupAddress);
401
402 connect(&m_socket, &QUdpSocket::readyRead, this, &SSDPReceiver::processPendingDatagrams);
403}
404
406{
407 while (m_socket.hasPendingDatagrams() && m_isRunning)
408 {
409 QNetworkDatagram datagram = m_socket.receiveDatagram();
410 QString str = QString::fromUtf8(datagram.data());
411 QStringList lines = str.split("\r\n", Qt::SkipEmptyParts);
412 QString sRequestLine = !lines.empty() ? lines[0] : "";
413
414 if (!lines.isEmpty())
415 lines.pop_front();
416
417 // Parse request Type
418 LOG(VB_UPNP, LOG_DEBUG, QString("SSDP::ProcessData - requestLine: %1")
419 .arg(sRequestLine));
420 SSDPRequestType eType = ProcessRequestLine( sRequestLine );
421
422 // Read Headers into map
423 QMap<QString, QString> headers;
424 for (const auto& sLine : std::as_const(lines))
425 {
426 QString sName = sLine.section(':', 0, 0).trimmed();
427 QString sValue = sLine.section(':', 1);
428
429 sValue.truncate(sValue.length()); //-2
430
431 if ((sName.length() != 0) && (sValue.length() != 0))
432 {
433 headers.insert(sName.toLower(), sValue.trimmed());
434 }
435 }
436
437 // See if this is a valid request
438 switch (eType)
439 {
440 case SSDP_MSearch:
441 {
442 ProcessSearchRequest(headers, datagram.senderAddress(),
443 datagram.senderPort(), SSDP::Instance()->getNotificationPort());
444
445 break;
446 }
447
448 case SSDP_MSearchResp:
450 break;
451
452 case SSDP_Notify:
454 break;
455
456 case SSDP_Unknown:
457 default:
458 LOG(VB_UPNP, LOG_ERR, "SSPD::ProcessData - Unknown request Type.");
459 break;
460 }
461 }
462}
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:267
void quit(void)
calls exit(0)
Definition: mthread.cpp:279
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:284
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:217
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
static SSDPCache * Instance()
Definition: ssdpcache.cpp:285
void Add(const QString &sURI, const QString &sUSN, const QString &sLocation, std::chrono::seconds sExpiresInSecs)
Definition: ssdpcache.cpp:372
void Remove(const QString &sURI, const QString &sUSN)
Definition: ssdpcache.cpp:459
QUdpSocket m_socket
Definition: ssdp.h:53
void processPendingDatagrams()
Definition: ssdp.cpp:405
void setIsRunning(bool isRunning)
Definition: ssdp.h:47
const QHostAddress m_groupAddress
Definition: ssdp.h:55
const uint16_t m_port
Definition: ssdp.h:54
bool m_isRunning
Definition: ssdp.h:56
void performSearch(const QString &sST, std::chrono::seconds timeout=2s)
Definition: ssdp.cpp:164
SSDPReceiver()
Definition: ssdp.cpp:396
Definition: ssdp.h:60
MThread m_thread
Definition: ssdp.h:70
void EnableNotifications(int nServicePort)
Definition: ssdp.cpp:110
void DisableNotifications()
Definition: ssdp.cpp:148
SSDP()
Definition: ssdp.cpp:78
int getNotificationPort() const
Definition: ssdp.h:95
~SSDP()
Definition: ssdp.cpp:89
SSDPReceiver m_receiver
Definition: ssdp.h:69
static SSDP * Instance()
Definition: ssdp.cpp:57
int m_nServicePort
Definition: ssdp.h:65
void PerformSearch(const QString &sST, std::chrono::seconds timeout=2s)
Send a SSDP discover multicast datagram.
Definition: ssdp.cpp:159
class UPnpNotifyTask * m_pNotifyTask
Definition: ssdp.h:67
static void Shutdown()
Definition: ssdp.cpp:67
static SSDP * g_pSSDP
Definition: ssdp.h:63
void AddTask(std::chrono::milliseconds msec, Task *pTask)
Add a task to run in the future.
Definition: taskqueue.cpp:170
static TaskQueue * Instance()
Definition: taskqueue.cpp:57
QString FindDeviceUDN(UPnpDevice *pDevice, QString sST)
Definition: upnpdevice.cpp:560
void SetNTS(UPnpNotifyNTS nts)
void Execute(TaskQueue *pQueue) override
static UPnpDeviceDesc g_UPnpDeviceDesc
Definition: upnp.h:48
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
Convenience inline random number generator functions.
uint32_t MythRandom()
generate 32 random bits
Definition: mythrandom.h:20
static eu8 clamp(eu8 value, eu8 low, eu8 high)
Definition: pxsup2dast.c:204
static bool ProcessNotify(const QMap< QString, QString > &headers)
Definition: ssdp.cpp:350
static SSDPRequestType ProcessRequestLine(const QString &sLine)
Definition: ssdp.cpp:193
static bool ProcessSearchResponse(const QMap< QString, QString > &headers)
Definition: ssdp.cpp:319
static QMutex g_pSSDPCreationLock
Definition: ssdp.cpp:50
static bool ProcessSearchRequest(const QMap< QString, QString > &sHeaders, const QHostAddress &peerAddress, quint16 peerPort, int servicePort)
Definition: ssdp.cpp:228
static QString GetHeaderValue(const QMap< QString, QString > &headers, const QString &sKey, const QString &sDefault)
Definition: ssdp.cpp:217
static constexpr uint16_t SSDP_PORT
Definition: ssdp.h:29
static constexpr const char * SSDP_GROUP
Definition: ssdp.h:28
SSDPRequestType
Definition: ssdp.h:32
@ SSDP_Notify
Definition: ssdp.h:36
@ SSDP_MSearchResp
Definition: ssdp.h:35
@ SSDP_Unknown
Definition: ssdp.h:33
@ SSDP_MSearch
Definition: ssdp.h:34
@ NTS_alive
@ NTS_byebye