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 LOG(VB_UPNP, LOG_NOTICE, "SSDP instance created." );
81}
82
84//
86
88{
89 LOG(VB_UPNP, LOG_NOTICE, "Destroying SSDP instance." );
90
92 if (m_pNotifyTask != nullptr)
93 {
95 m_pNotifyTask = nullptr;
96 }
97
98 LOG(VB_UPNP, LOG_INFO, "SSDP instance destroyed." );
99}
100
102//
104
105void SSDP::EnableNotifications( int nServicePort )
106{
107 if ( m_pNotifyTask == nullptr )
108 {
109 m_nServicePort = nServicePort;
110
111 LOG(VB_UPNP, LOG_INFO,
112 "SSDP::EnableNotifications() - creating new task");
114
115 // ------------------------------------------------------------------
116 // First Send out Notification that we are leaving the network.
117 // ------------------------------------------------------------------
118
119 LOG(VB_UPNP, LOG_INFO,
120 "SSDP::EnableNotifications() - sending NTS_byebye");
122 m_pNotifyTask->Execute( nullptr );
123 }
124
125 // ------------------------------------------------------------------
126 // Add Announcement Task to the Queue
127 // ------------------------------------------------------------------
128
129 LOG(VB_UPNP, LOG_INFO, "SSDP::EnableNotifications() - sending NTS_alive");
130
132
134
135 LOG(VB_UPNP, LOG_INFO,
136 "SSDP::EnableNotifications() - Task added to UPnP queue");
137}
138
140//
142
144{
145 if (m_pNotifyTask != nullptr)
146 {
147 // Send Announcement that we are leaving.
148
150 m_pNotifyTask->Execute( nullptr );
151 }
152}
153
154void SSDP::PerformSearch(const QString &sST, std::chrono::seconds timeout)
155{
157}
158
159void SSDPReceiver::performSearch(const QString &sST, std::chrono::seconds timeout)
160{
161 timeout = std::clamp(timeout, 1s, 5s);
162 QString rRequest = QString("M-SEARCH * HTTP/1.1\r\n"
163 "HOST: 239.255.255.250:1900\r\n"
164 "MAN: \"ssdp:discover\"\r\n"
165 "MX: %1\r\n"
166 "ST: %2\r\n"
167 "\r\n")
168 .arg(timeout.count()).arg(sST);
169
170 LOG(VB_UPNP, LOG_DEBUG, QString("\n\n%1\n").arg(rRequest));
171
172 QByteArray sRequest = rRequest.toUtf8();
173 int nSize = sRequest.size();
174
175 if (m_socket.writeDatagram(sRequest, QHostAddress(QString(SSDP_GROUP)), SSDP_PORT) != nSize)
176 {
177 LOG(VB_GENERAL, LOG_INFO, "SSDP::PerformSearch - did not write entire buffer.");
178 }
179
180 std::this_thread::sleep_for(std::chrono::milliseconds(MythRandom(0, 250)));
181
182 if (m_socket.writeDatagram(sRequest, QHostAddress(QString(SSDP_GROUP)), SSDP_PORT) != nSize)
183 {
184 LOG(VB_GENERAL, LOG_INFO, "SSDP::PerformSearch - did not write entire buffer.");
185 }
186}
187
188static SSDPRequestType ProcessRequestLine(const QString &sLine)
189{
190 static const QRegularExpression k_whitespace {"\\s+"};
191 QStringList tokens = sLine.split(k_whitespace, Qt::SkipEmptyParts);
192
193 // ----------------------------------------------------------------------
194 // if this is actually a response, then sLine's format will be:
195 // HTTP/m.n <response code> <response text>
196 // otherwise:
197 // <method> <Resource URI> HTTP/m.n
198 // ----------------------------------------------------------------------
199
200 if ( sLine.startsWith( QString("HTTP/") ))
201 return SSDP_MSearchResp;
202
203 if (tokens.count() > 0)
204 {
205 if (tokens[0] == "M-SEARCH" ) return SSDP_MSearch;
206 if (tokens[0] == "NOTIFY" ) return SSDP_Notify;
207 }
208
209 return SSDP_Unknown;
210}
211
212static QString GetHeaderValue(const QMap<QString, QString> &headers,
213 const QString &sKey, const QString &sDefault )
214{
215 QMap<QString, QString>::const_iterator it = headers.find(sKey.toLower());
216
217 if ( it == headers.end())
218 return( sDefault );
219
220 return *it;
221}
222
223static bool ProcessSearchRequest(const QMap<QString, QString> &sHeaders,
224 const QHostAddress& peerAddress,
225 quint16 peerPort,
226 int servicePort)
227{
228 // Don't respond if notifications are not enabled.
229 if (servicePort == 0)
230 {
231 return true;
232 }
233
234 QString sMAN = GetHeaderValue( sHeaders, "MAN", "" );
235 QString sST = GetHeaderValue( sHeaders, "ST" , "" );
236 QString sMX = GetHeaderValue( sHeaders, "MX" , "" );
237 std::chrono::seconds nMX = 0s;
238
239 LOG(VB_UPNP, LOG_DEBUG, QString("SSDP::ProcessSearchrequest : [%1] MX=%2")
240 .arg(sST, sMX));
241
242 // ----------------------------------------------------------------------
243 // Validate Header Values...
244 // ----------------------------------------------------------------------
245
246#if 0
247 if ( pRequest->m_sMethod != "*" ) return false;
248 if ( pRequest->m_sProtocol != "HTTP" ) return false;
249 if ( pRequest->m_nMajor != 1 ) return false;
250#endif
251 if ( sMAN != "\"ssdp:discover\"" ) return false;
252 if ( sST.length() == 0 ) return false;
253 if ( sMX.length() == 0 ) return false;
254 nMX = std::chrono::seconds(sMX.toInt());
255 if ( nMX <= 0s ) return false;
256
257 // ----------------------------------------------------------------------
258 // Adjust timeout to be a random interval between 0 and MX (max of 120)
259 // ----------------------------------------------------------------------
260
261 nMX = std::clamp(nMX, 0s, 120s);
262
263 auto nNewMX = std::chrono::milliseconds(MythRandom(0, (duration_cast<std::chrono::milliseconds>(nMX)).count()));
264
265 // ----------------------------------------------------------------------
266 // See what they are looking for...
267 // ----------------------------------------------------------------------
268
269 if ((sST == "ssdp:all") || (sST == "upnp:rootdevice"))
270 {
271 auto *pTask = new UPnpSearchTask(servicePort,
272 peerAddress, peerPort, sST,
273 UPnp::g_UPnpDeviceDesc.m_rootDevice.GetUDN());
274
275#if 0
276 // Excute task now for fastest response, queue for time-delayed response
277 // -=>TODO: To be trully uPnp compliant, this Execute should be removed.
278 pTask->Execute( nullptr );
279#endif
280
281 TaskQueue::Instance()->AddTask( nNewMX, pTask );
282
283 pTask->DecrRef();
284
285 return true;
286 }
287
288 // ----------------------------------------------------------------------
289 // Look for a specific device/service
290 // ----------------------------------------------------------------------
291
293 &(UPnp::g_UPnpDeviceDesc.m_rootDevice), sST );
294
295 if (sUDN.length() > 0)
296 {
297 auto *pTask = new UPnpSearchTask(servicePort, peerAddress,
298 peerPort, sST, sUDN );
299
300 // Excute task now for fastest response, queue for time-delayed response
301 // -=>TODO: To be trully uPnp compliant, this Execute should be removed.
302 pTask->Execute( nullptr );
303
304 TaskQueue::Instance()->AddTask( nNewMX, pTask );
305
306 pTask->DecrRef();
307
308 return true;
309 }
310
311 return false;
312}
313
314static bool ProcessSearchResponse(const QMap<QString, QString> &headers)
315{
316 QString sDescURL = GetHeaderValue( headers, "LOCATION" , "" );
317 QString sST = GetHeaderValue( headers, "ST" , "" );
318 QString sUSN = GetHeaderValue( headers, "USN" , "" );
319 QString sCache = GetHeaderValue( headers, "CACHE-CONTROL" , "" );
320
321 LOG(VB_UPNP, LOG_DEBUG,
322 QString( "SSDP::ProcessSearchResponse ...\n"
323 "DescURL=%1\n"
324 "ST =%2\n"
325 "USN =%3\n"
326 "Cache =%4")
327 .arg(sDescURL, sST, sUSN, sCache));
328
329 int nPos = sCache.indexOf("max-age", 0, Qt::CaseInsensitive);
330
331 if (nPos < 0)
332 return false;
333
334 nPos = sCache.indexOf("=", nPos);
335 if (nPos < 0)
336 return false;
337
338 auto nSecs = std::chrono::seconds(sCache.mid( nPos+1 ).toInt());
339
340 SSDPCache::Instance()->Add( sST, sUSN, sDescURL, nSecs );
341
342 return true;
343}
344
345static bool ProcessNotify(const QMap<QString, QString> &headers)
346{
347 QString sDescURL = GetHeaderValue( headers, "LOCATION" , "" );
348 QString sNTS = GetHeaderValue( headers, "NTS" , "" );
349 QString sNT = GetHeaderValue( headers, "NT" , "" );
350 QString sUSN = GetHeaderValue( headers, "USN" , "" );
351 QString sCache = GetHeaderValue( headers, "CACHE-CONTROL" , "" );
352
353 LOG(VB_UPNP, LOG_DEBUG,
354 QString( "SSDP::ProcessNotify ...\n"
355 "DescURL=%1\n"
356 "NTS =%2\n"
357 "NT =%3\n"
358 "USN =%4\n"
359 "Cache =%5" )
360 .arg(sDescURL, sNTS, sNT, sUSN, sCache));
361
362 if (sNTS.contains( "ssdp:alive"))
363 {
364 int nPos = sCache.indexOf("max-age", 0, Qt::CaseInsensitive);
365
366 if (nPos < 0)
367 return false;
368
369 nPos = sCache.indexOf("=", nPos);
370 if (nPos < 0)
371 return false;
372
373 auto nSecs = std::chrono::seconds(sCache.mid( nPos+1 ).toInt());
374
375 SSDPCache::Instance()->Add( sNT, sUSN, sDescURL, nSecs );
376
377 return true;
378 }
379
380
381 if ( sNTS.contains( "ssdp:byebye" ) )
382 {
383 SSDPCache::Instance()->Remove( sNT, sUSN );
384
385 return true;
386 }
387
388 return false;
389}
390
392 m_port(XmlConfiguration().GetValue("UPnP/SSDP/Port", SSDP_PORT))
393{
394 m_socket.bind(QHostAddress::AnyIPv4, m_port, QUdpSocket::ShareAddress);
395 m_socket.joinMulticastGroup(m_groupAddress);
396
397 connect(&m_socket, &QUdpSocket::readyRead, this, &SSDPReceiver::processPendingDatagrams);
398}
399
401{
402 while (m_socket.hasPendingDatagrams())
403 {
404 QNetworkDatagram datagram = m_socket.receiveDatagram();
405 QString str = QString::fromUtf8(datagram.data());
406 QStringList lines = str.split("\r\n", Qt::SkipEmptyParts);
407 QString sRequestLine = !lines.empty() ? lines[0] : "";
408
409 if (!lines.isEmpty())
410 lines.pop_front();
411
412 // Parse request Type
413 LOG(VB_UPNP, LOG_DEBUG, QString("SSDP::ProcessData - requestLine: %1")
414 .arg(sRequestLine));
415 SSDPRequestType eType = ProcessRequestLine( sRequestLine );
416
417 // Read Headers into map
418 QMap<QString, QString> headers;
419 for (const auto& sLine : std::as_const(lines))
420 {
421 QString sName = sLine.section(':', 0, 0).trimmed();
422 QString sValue = sLine.section(':', 1);
423
424 sValue.truncate(sValue.length()); //-2
425
426 if ((sName.length() != 0) && (sValue.length() != 0))
427 {
428 headers.insert(sName.toLower(), sValue.trimmed());
429 }
430 }
431
432 // See if this is a valid request
433 switch (eType)
434 {
435 case SSDP_MSearch:
436 {
437 ProcessSearchRequest(headers, datagram.senderAddress(),
438 datagram.senderPort(), SSDP::Instance()->getNotificationPort());
439
440 break;
441 }
442
443 case SSDP_MSearchResp:
445 break;
446
447 case SSDP_Notify:
449 break;
450
451 case SSDP_Unknown:
452 default:
453 LOG(VB_UPNP, LOG_ERR, "SSPD::ProcessData - Unknown request Type.");
454 break;
455 }
456 }
457}
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:457
QUdpSocket m_socket
Definition: ssdp.h:50
void processPendingDatagrams()
Definition: ssdp.cpp:400
const QHostAddress m_groupAddress
Definition: ssdp.h:52
const uint16_t m_port
Definition: ssdp.h:51
void performSearch(const QString &sST, std::chrono::seconds timeout=2s)
Definition: ssdp.cpp:159
SSDPReceiver()
Definition: ssdp.cpp:391
Definition: ssdp.h:56
void EnableNotifications(int nServicePort)
Definition: ssdp.cpp:105
void DisableNotifications()
Definition: ssdp.cpp:143
SSDP()
Definition: ssdp.cpp:78
int getNotificationPort() const
Definition: ssdp.h:90
~SSDP()
Definition: ssdp.cpp:87
SSDPReceiver m_receiver
Definition: ssdp.h:65
static SSDP * Instance()
Definition: ssdp.cpp:57
int m_nServicePort
Definition: ssdp.h:61
void PerformSearch(const QString &sST, std::chrono::seconds timeout=2s)
Send a SSDP discover multicast datagram.
Definition: ssdp.cpp:154
class UPnpNotifyTask * m_pNotifyTask
Definition: ssdp.h:63
static void Shutdown()
Definition: ssdp.cpp:67
static SSDP * g_pSSDP
Definition: ssdp.h:59
void AddTask(std::chrono::milliseconds msec, Task *pTask)
Add a task to run in the future.
Definition: taskqueue.cpp:168
static TaskQueue * Instance()
Definition: taskqueue.cpp:55
QString FindDeviceUDN(UPnpDevice *pDevice, QString sST)
Definition: upnpdevice.cpp:557
void SetNTS(UPnpNotifyNTS nts)
void Execute(TaskQueue *pQueue) override
static UPnpDeviceDesc g_UPnpDeviceDesc
Definition: upnp.h:107
#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:206
static bool ProcessNotify(const QMap< QString, QString > &headers)
Definition: ssdp.cpp:345
static SSDPRequestType ProcessRequestLine(const QString &sLine)
Definition: ssdp.cpp:188
static bool ProcessSearchResponse(const QMap< QString, QString > &headers)
Definition: ssdp.cpp:314
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:223
static QString GetHeaderValue(const QMap< QString, QString > &headers, const QString &sKey, const QString &sDefault)
Definition: ssdp.cpp:212
static constexpr uint16_t SSDP_PORT
Definition: ssdp.h:27
static constexpr const char * SSDP_GROUP
Definition: ssdp.h:26
SSDPRequestType
Definition: ssdp.h:30
@ SSDP_Notify
Definition: ssdp.h:34
@ SSDP_MSearchResp
Definition: ssdp.h:33
@ SSDP_Unknown
Definition: ssdp.h:31
@ SSDP_MSearch
Definition: ssdp.h:32
@ NTS_alive
@ NTS_byebye