Torc  0.1
torcssdp.cpp
Go to the documentation of this file.
1 /* Class TorcSSDP
2 *
3 * This file is part of the Torc project.
4 *
5 * Copyright (C) Mark Kendall 2013-18
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
20 * USA.
21 */
22 
23 // Qt
24 #include <QHostAddress>
25 
26 // Torc
27 #include "torclocalcontext.h"
28 #include "torchttpserver.h"
29 #include "torccoreutils.h"
30 #include "torclogging.h"
31 #include "torcadminthread.h"
32 #include "torcnetwork.h"
33 #include "torcupnp.h"
34 #include "torcssdp.h"
35 
36 TorcSSDP* TorcSSDP::gSSDP = nullptr;
37 QMutex* TorcSSDP::gSSDPLock = new QMutex(QMutex::Recursive);
38 bool TorcSSDP::gSearchEnabled = false;
40 bool TorcSSDP::gAnnounceEnabled = false;
42 
43 TorcSSDPSearchResponse::TorcSSDPSearchResponse(const QHostAddress &Address, const ResponseTypes Types, int Port)
44  : m_responseAddress(Address),
45  m_responseTypes(Types),
46  m_port(Port)
47 {
48 }
49 
51  : m_responseAddress(QHostAddress(QHostAddress::Any)),
53  m_port(0)
54 {
55 }
56 
64  : QObject(),
65  m_searchOptions(),
66  m_announceOptions(),
67  m_serverString(),
68  m_searching(false),
69  m_searchDebugged(false),
70  m_firstSearchTimer(0),
71  m_secondSearchTimer(0),
72  m_firstAnnounceTimer(0),
73  m_secondAnnounceTimer(0),
74  m_refreshTimer(0),
75  m_started(false),
76  m_ipv4Address(),
77  m_ipv6Address(),
78  m_addressess(),
79  m_ipv4GroupAddress(TORC_IPV4_UDP_MULTICAST_ADDR),
80  m_ipv4MulticastSocket(nullptr),
81  m_ipv6LinkGroupBaseAddress(TORC_IPV6_UDP_MULTICAST_ADDR2),
82  m_ipv6LinkMulticastSocket(nullptr),
83  m_discoveredDevices(),
84  m_ipv4UnicastSocket(nullptr),
85  m_ipv6UnicastSocket(nullptr),
86  m_responseTimer(),
87  m_responseQueue()
88 {
89 #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
90  m_serverString = QStringLiteral("%1/%2 UPnP/1.0 Torc/0.1").arg(QSysInfo::kernelType(), QSysInfo::kernelVersion());
91 #else
92  m_serverString = QStringLiteral("OldTorc/OldVersion UPnP/1.0 Torc/0.1");
93 #endif
95 
97  Start();
98 
99  // minimum advertisement duration is 30 mins (1800 seconds). Check for expiry
100  // every 5 minutes and flush stale entries
101  m_refreshTimer = startTimer(5 * 60 * 1000, Qt::VeryCoarseTimer);
102 
103  // start the response timer here and connect its signal
104  // we use a full QTimer as we need to know how long it has remaining
105  connect(&m_responseTimer, &QTimer::timeout, this, &TorcSSDP::ProcessResponses);
106  m_responseTimer.start(5000);
107 }
108 
110 {
111  Stop();
112 
113  if (m_refreshTimer)
114  killTimer(m_refreshTimer);
115 
117 }
118 
119 void TorcSSDP::Start(void)
120 {
121  if (m_started)
122  Stop();
123 
124  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Starting SSDP as %1").arg(m_serverString));
125 
126  // try and get the interface the network is using
127  QNetworkInterface interface;
128  QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
129  foreach (const QNetworkInterface &i, interfaces)
130  {
131  QNetworkInterface::InterfaceFlags flags = i.flags();
132  if (flags & QNetworkInterface::IsUp &&
133  flags & QNetworkInterface::IsRunning &&
134  flags & QNetworkInterface::CanMulticast &&
135  !(flags & QNetworkInterface::IsLoopBack))
136  {
137  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Using interface '%1' for multicast").arg(i.name()));
138  interface = i;
139  break;
140  }
141  }
142 
143  // create the list of known own addresses
144  m_ipv4Address = QString();
145  m_ipv6Address = QString();
146  m_addressess = QNetworkInterface::allAddresses();
147 
148  // try and determine suitable ipv4 and ipv6 addresses
149  // at the moment I cannot test 'proper' IPv6 support, so allow link local for now
150  QString ipv6linklocal;
151  foreach (QHostAddress address, m_addressess)
152  {
153  if (TorcNetwork::IsGlobal(address))
154  continue;
155  if (!TorcNetwork::IsExternal(address))
156  continue;
157  if (TorcNetwork::IsLocal(address))
158  {
159  if (address.protocol() == QAbstractSocket::IPv4Protocol)
160  m_ipv4Address = address.toString();
161  else
162  m_ipv6Address = address.toString();
163  }
164  else if (TorcNetwork::IsLinkLocal(address) && address.protocol() == QAbstractSocket::IPv6Protocol)
165  {
166  ipv6linklocal = address.toString();
167  }
168  }
169 
170  // fall back to link local for ipv6
171  if (m_ipv6Address.isEmpty() && !ipv6linklocal.isEmpty())
172  m_ipv6Address = ipv6linklocal;
173  if (!m_ipv6Address.isEmpty() && !m_ipv6Address.startsWith('['))
174  m_ipv6Address = QStringLiteral("[%1]").arg(m_ipv6Address);
175 
176  if (m_ipv4Address.isEmpty())
177  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Disabling SSDP IPv4 - no valid address detected"));
178  else
179  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("SSDP IPv4 host url: %1").arg(m_ipv4Address));
180  if (m_ipv6Address.isEmpty())
181  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Disabling SSDP IPv6 - no valid address detected"));
182  else
183  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("SSDP IPv6 host url: %1 (if enabled)").arg(m_ipv6Address));
184 
185  // create sockets
186  if (interface.isValid())
187  {
188  m_ipv4MulticastSocket = CreateMulticastSocket(m_ipv4GroupAddress, this, interface);
189  m_ipv6LinkMulticastSocket = CreateMulticastSocket(m_ipv6LinkGroupBaseAddress, this, interface);
190  m_ipv4UnicastSocket = CreateSearchSocket(QHostAddress(QHostAddress::AnyIPv4), this);
191  m_ipv6UnicastSocket = CreateSearchSocket(QHostAddress(QHostAddress::AnyIPv6), this);
192  }
193  else
194  {
195  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Failed to create SSDP sockets - no suitable network interface"));
196  }
197 
198  // check whether search or announce was requested before we started
199  bool search;
200  bool announce;
201  {
202  QMutexLocker locker(gSSDPLock);
203  search = gSearchEnabled;
204  announce = gAnnounceEnabled;
205  }
206 
207  if (search)
208  StartSearch();
209 
210  if (announce)
211  StartAnnounce();
212 
213  m_started = true;
214 }
215 
216 void TorcSSDP::Stop(void)
217 {
218  // cancel search and announce
219  StopSearch();
220  StopAnnounce();
221 
222  // discard queued responses
223  m_responseQueue.clear();
224 
225  // Network is no longer available - notify clients that services have gone away
226  QHash<QString,TorcUPNPDescription>::const_iterator it2 = m_discoveredDevices.constBegin();
227  for ( ; it2 != m_discoveredDevices.constEnd(); ++it2)
228  {
229  QVariantMap data;
230  data.insert(TORC_USN, it2.value().GetUSN());
232  gLocalContext->Notify(event);
233  }
234  m_discoveredDevices.clear();
235 
236  m_addressess.clear();
237  m_ipv4Address = QString();
238  m_ipv6Address = QString();
239 
240  if (m_started)
241  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Stopping SSDP"));
242  m_started = false;
243 
244  if (m_ipv4MulticastSocket)
245  m_ipv4MulticastSocket->close();
246  if (m_ipv6LinkMulticastSocket)
247  m_ipv6LinkMulticastSocket->close();
248  if (m_ipv4UnicastSocket)
249  m_ipv4UnicastSocket->close();
250  if (m_ipv6UnicastSocket)
251  m_ipv6UnicastSocket->close();
252 
253  delete m_ipv4MulticastSocket;
254  delete m_ipv6LinkMulticastSocket;
255  delete m_ipv4UnicastSocket;
256  delete m_ipv6UnicastSocket;
257 
258  m_ipv4MulticastSocket = nullptr;
259  m_ipv6LinkMulticastSocket = nullptr;
260  m_ipv4UnicastSocket = nullptr;
261  m_ipv6UnicastSocket = nullptr;
262 }
263 
270 {
271  QMutexLocker locker(gSSDPLock);
272  gSearchEnabled = true;
273  gSearchOptions = Options;
274  if (gSSDP)
275  QMetaObject::invokeMethod(gSSDP, "SearchPriv", Qt::AutoConnection);
276 }
277 
284 {
285  QMutexLocker locker(gSSDPLock);
286  gSearchEnabled = false;
287  if (gSSDP)
288  QMetaObject::invokeMethod(gSSDP, "CancelSearchPriv", Qt::AutoConnection);
289 }
290 
295 {
296  QMutexLocker locker(gSSDPLock);
297  gAnnounceEnabled = true;
298  gAnnounceOptions = Options;
299  if (gSSDP)
300  QMetaObject::invokeMethod(gSSDP, "AnnouncePriv", Qt::AutoConnection);
301 }
302 
306 {
307  QMutexLocker locker(gSSDPLock);
308  gAnnounceEnabled = false;
310  if (gSSDP)
311  QMetaObject::invokeMethod(gSSDP, "CancelAnnouncePriv", Qt::AutoConnection);
312 }
313 
315 {
316  QMutexLocker locker(gSSDPLock);
317 
318  if (!Destroy)
319  {
320  if (!gSSDP)
321  gSSDP = new TorcSSDP();
322  return gSSDP;
323  }
324 
325  delete gSSDP;
326  gSSDP = nullptr;
327  return nullptr;
328 }
329 
330 #define IPv4 QStringLiteral("IPv4")
331 #define IPv6 QStringLiteral("IPv6")
332 QUdpSocket* TorcSSDP::CreateSearchSocket(const QHostAddress &HostAddress, TorcSSDP *Owner)
333 {
334  QUdpSocket *socket = new QUdpSocket();
335  if (socket->bind(HostAddress, 1900, QUdpSocket::ShareAddress))
336  {
337  socket->setSocketOption(QAbstractSocket::MulticastTtlOption, 4);
338  socket->setSocketOption(QAbstractSocket::MulticastLoopbackOption, 1);
339  QObject::connect(socket, &QUdpSocket::readyRead, Owner, &TorcSSDP::Read);
340  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("%1 SSDP unicast socket %2:%3")
341  .arg(HostAddress.protocol() == QAbstractSocket::IPv6Protocol ? IPv6 : IPv4,
342  socket->localAddress().toString(), QString::number(socket->localPort())));
343  return socket;
344  }
345 
346  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to bind %1 SSDP search socket with address %3 (%2)")
347  .arg(HostAddress.protocol() == QAbstractSocket::IPv6Protocol ? IPv6 : IPv4, socket->errorString(), HostAddress.toString()));
348  delete socket;
349  return nullptr;
350 }
351 
352 QUdpSocket* TorcSSDP::CreateMulticastSocket(const QHostAddress &HostAddress, TorcSSDP *Owner, const QNetworkInterface &Interface)
353 {
354  QUdpSocket *socket = new QUdpSocket();
355  if (socket->bind(HostAddress, TORC_SSDP_UDP_MULTICAST_PORT, QUdpSocket::ShareAddress))
356  {
357  if (socket->joinMulticastGroup(HostAddress, Interface))
358  {
359  QObject::connect(socket, &QUdpSocket::readyRead, Owner, &TorcSSDP::Read);
360  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("%1 SSDP multicast socket %2:%3")
361  .arg(HostAddress.protocol() == QAbstractSocket::IPv6Protocol ? IPv6 : IPv4,
362  socket->localAddress().toString(),
363  QString::number(socket->localPort())));
364  return socket;
365  }
366 
367  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to join %1 multicast group (%2)")
368  .arg(HostAddress.protocol() == QAbstractSocket::IPv6Protocol ? IPv6 : IPv4, socket->errorString()));
369  }
370  else
371  {
372  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to bind %1 SSDP multicast socket with address %3 (%2)")
373  .arg(HostAddress.protocol() == QAbstractSocket::IPv6Protocol ? IPv6 : IPv4, socket->errorString(), HostAddress.toString()));
374  }
375 
376  delete socket;
377  return nullptr;
378 }
379 
381 {
382  // evented when the network is available or we have restarted and then twice every five minutes
383  // we only search for the Torc root device
384 
385  QString searchdevice = TORC_ROOT_UPNP_DEVICE;//"ssdp:all";
386  static const QString search(QStringLiteral(
387  "M-SEARCH * HTTP/1.1\r\n"
388  "HOST: %1\r\n"
389  "MAN: \"ssdp:discover\"\r\n"
390  "MX: 3\r\n"
391  "ST: %2\r\n\r\n"));
392 
393  if (!m_ipv4Address.isEmpty() && m_ipv4MulticastSocket && m_ipv4MulticastSocket->isValid() && m_ipv4MulticastSocket->state() == QAbstractSocket::BoundState)
394  {
395  m_ipv4MulticastSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 2);
396  QByteArray ipv4search = QString(search).arg(TORC_IPV4_UDP_MULTICAST_URL, searchdevice).toLocal8Bit();
397  qint64 sent = m_ipv4MulticastSocket->writeDatagram(ipv4search, m_ipv4GroupAddress, TORC_SSDP_UDP_MULTICAST_PORT);
398  if (sent != ipv4search.size())
399  {
400  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Error sending IPv4 search request (%1)").arg(m_ipv4MulticastSocket->errorString()));
401  }
402  else if (!m_searchDebugged)
403  {
404  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Sent IPv4 SSDP global search request"));
405  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Will send further IPv4 requests every 5 minutes"));
406  }
407  }
408 
409  if (m_searchOptions.ipv6 && !m_ipv6Address.isEmpty() && m_ipv6LinkMulticastSocket && m_ipv6LinkMulticastSocket->isValid() && m_ipv6LinkMulticastSocket->state() == QAbstractSocket::BoundState)
410  {
411  m_ipv6LinkMulticastSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 2);
412  QByteArray ipv6search = QString(search).arg(TORC_IPV6_UDP_MULTICAST_URL, searchdevice).toLocal8Bit();
413  qint64 sent = m_ipv6LinkMulticastSocket->writeDatagram(ipv6search, m_ipv6LinkGroupBaseAddress, TORC_SSDP_UDP_MULTICAST_PORT);
414  if (sent != ipv6search.size())
415  {
416  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Error sending IPv6 search request (%1)").arg(m_ipv6LinkMulticastSocket->errorString()));
417  }
418  else if (!m_searchDebugged)
419  {
420  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Sent IPv6 SSDP global search request"));
421  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Will send further IPv6 requests every 5 minutes"));
422  }
423  }
424  m_searchDebugged = true;
425 }
426 
428 {
429  // if we haven't started or the network is unavailable, the search will be triggered when we restart
430  if (m_started && TorcNetwork::IsAvailable())
431  StartSearch();
432 }
433 
435 {
436  StopSearch();
437 }
438 
440 {
441  // if we haven't started or the network is unavailable, announce will be triggered when we restart
442  if (m_started && TorcNetwork::IsAvailable())
443  StartAnnounce();
444 }
445 
447 {
448  StopAnnounce();
449 }
450 
451 void TorcSSDP::StartAnnounce(void)
452 {
453  gSSDPLock->lock();
455  gSSDPLock->unlock();
456 
457  if (m_announceOptions == newoptions)
458  {
459  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("SSDP announce options unchanged - not restarting"));
460  return;
461  }
462 
463  StopAnnounce();
464  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Starting SSDP announce (%1)").arg(TORC_ROOT_UPNP_DEVICE));
465  m_announceOptions = newoptions;
466 
467  // schedule first announce almost immediately
468  m_firstAnnounceTimer = startTimer(100, Qt::CoarseTimer);
469  // and schedule another shortly afterwards
470  m_secondAnnounceTimer = startTimer(500, Qt::CoarseTimer);
471 
472 }
473 
474 void TorcSSDP::StopAnnounce(void)
475 {
476  if (m_announceOptions.port)
477  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Stopping SSDP announce (%1)").arg(TORC_ROOT_UPNP_DEVICE));
478 
479  if (m_firstAnnounceTimer)
480  {
481  killTimer(m_firstAnnounceTimer);
482  m_firstAnnounceTimer = 0;
483  }
484 
485  if (m_secondAnnounceTimer)
486  {
487  killTimer(m_secondAnnounceTimer);
488  m_secondAnnounceTimer = 0;
489  }
490 
491  // try and send out byebye
492  if (m_announceOptions.port)
493  {
494  SendAnnounce(false, false); // IPv4 byebye
495  if (m_announceOptions.ipv6)
496  SendAnnounce(true, false); // IPv6 byebye
497  }
498 
499  m_announceOptions.port = 0;
500 }
501 
502 void TorcSSDP::SendAnnounce(bool IsIPv6, bool Alive)
503 {
504  if (!m_announceOptions.port)
505  {
506  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Cannot send announce - no port set"));
507  return;
508  }
509 
510  QUdpSocket *socket = IsIPv6 ? m_ipv6LinkMulticastSocket : m_ipv4MulticastSocket;
511 
512  if ((IsIPv6 && m_ipv6Address.isEmpty()) || (!IsIPv6 && m_ipv4Address.isEmpty()))
513  return;
514 
515  if (!socket || !socket->isValid() || socket->state() != QAbstractSocket::BoundState)
516  return;
517 
518  /* We need to send out 3 packets and then repeat after a short period of time.
519  * Packet 1 - NT: upnp:rootdevice USN: uuid:device-UUID::upnp:rootdevice
520  * Packet 2 - NT: uuid:device-UUID USN: uuid:device-UUID
521  * Packet 3 - NT: urn:schemas-torcapp-org:device:TorcClient:1 USN: uuid:device-UUID::urn:schemas-torcapp-org:device:TorcClient:1
522  */
523 
524  static const QString notify(QStringLiteral(
525  "NOTIFY * HTTP/1.1\r\n"
526  "HOST: %1\r\n"
527  "CACHE-CONTROL: max-age=1800\r\n"
528  "LOCATION: http%2://%3:%4/upnp/description\r\n"
529  "NT: %5\r\n"
530  "NTS: ssdp:%6\r\n"
531  "SERVER: %7\r\n"
532  "USN: %8\r\n"
533  "NAME: %9\r\n"
534  "APIVERSION: %10\r\n"
535  "STARTTIME: %11\r\n"
536  "PRIORITY: %12\r\n%13\r\n"));
537 
539  QHostAddress address = IsIPv6 ? m_ipv6LinkGroupBaseAddress : m_ipv4GroupAddress;
540  QString uuid = QStringLiteral("uuid:") + gLocalContext->GetUuid();
541  QString alive = Alive ? QStringLiteral("alive") :QStringLiteral("byebye");
542  QString url = IsIPv6 ? m_ipv6Address : m_ipv4Address;
543  QString secure = m_announceOptions.secure ? QStringLiteral("s") : QStringLiteral("");
544  QString secure2 = m_announceOptions.secure ? QStringLiteral("SECURE: yes\r\n") : QStringLiteral("");
545  QString name = TorcHTTPServer::ServerDescription();
546  QString apiversion = TorcHTTPServices::GetVersion();
547  QString starttime = QString::number(gLocalContext->GetStartTime());
548  QString priority = QString::number(gLocalContext->GetPriority());
549  QByteArray packet1 = QString(notify).arg(ip, secure, url).arg(m_announceOptions.port)
550  .arg(QStringLiteral("upnp:rootdevice"), alive, m_serverString, QStringLiteral("%1::upnp::rootdevice").arg(uuid), name, apiversion, starttime, priority, secure2).toLocal8Bit();
551  QByteArray packet2 = QString(notify).arg(ip, secure, url).arg(m_announceOptions.port)
552  .arg(uuid, alive, m_serverString, uuid, name, apiversion, starttime, priority, secure2).toLocal8Bit();
553  QByteArray packet3 = QString(notify).arg(ip, secure, url).arg(m_announceOptions.port)
554  .arg(TORC_ROOT_UPNP_DEVICE, alive, m_serverString, QStringLiteral("%1::%2").arg(uuid , TORC_ROOT_UPNP_DEVICE), name, apiversion, starttime, priority, secure2).toLocal8Bit();
555 
556  socket->setSocketOption(QAbstractSocket::MulticastTtlOption, 4);
557  qint64 sent = socket->writeDatagram(packet1, address, TORC_SSDP_UDP_MULTICAST_PORT);
558  sent += socket->writeDatagram(packet2, address, TORC_SSDP_UDP_MULTICAST_PORT);
559  sent += socket->writeDatagram(packet3, address, TORC_SSDP_UDP_MULTICAST_PORT);
560  if (sent != (packet1.size() + packet2.size() + packet3.size()))
561  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Error sending 3 %1 SSDP NOTIFYs (%2)").arg(IsIPv6 ? IPv6 : IPv4, m_ipv4MulticastSocket->errorString()));
562  else
563  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("Sent 3 %1 SSDP NOTIFYs ").arg(IsIPv6 ? IPv6 : IPv4));
564 }
565 
566 bool TorcSSDP::event(QEvent *Event)
567 {
568  if (Event->type() == TorcEvent::TorcEventType)
569  {
570  TorcEvent* event = dynamic_cast<TorcEvent*>(Event);
571  if (event)
572  {
573  if (event->GetEvent() == Torc::NetworkAvailable)
574  {
575  Start();
576  }
577  else if (event->GetEvent() == Torc::NetworkUnavailable)
578  {
579  Stop();
580  }
581  }
582  }
583  else if (Event->type() == QEvent::Timer)
584  {
585  QTimerEvent* event = dynamic_cast<QTimerEvent*>(Event);
586  if (event)
587  {
588  // N.B. we kill the timers as we don't know whether they were the initial, short
589  // duration timers or not
590 
591  // 2 search timers will be active if enabled, 5 seconds apart.
592  // reschedule them for 5 minutes
593  if (event->timerId() == m_firstSearchTimer)
594  {
595  killTimer(m_firstSearchTimer);
596  SendSearch();
597  m_firstSearchTimer = startTimer(5 * 60 * 1000);
598  }
599  else if (event->timerId() == m_secondSearchTimer)
600  {
601  killTimer(m_secondSearchTimer);
602  SendSearch();
603  m_secondSearchTimer = startTimer(5 * 60 * 1000);
604  }
605  // 2 announce timers will be active if enabled - a short time apart
606  // reshechedule them for every 10 minutes - although max-age is set to 30 mins (1800 seconds)
607  else if (event->timerId() == m_firstAnnounceTimer)
608  {
609  killTimer(m_firstAnnounceTimer);
610  SendAnnounce(false, true); //IPv4 alive
611  if (m_announceOptions.ipv6)
612  SendAnnounce(true, true); //IPv6 alive
613  m_firstAnnounceTimer = startTimer(10 * 60 * 1000);
614  }
615  else if (event->timerId() == m_secondAnnounceTimer)
616  {
617  killTimer(m_secondAnnounceTimer);
618  SendAnnounce(false, true);
619  if (m_announceOptions.ipv6)
620  SendAnnounce(true, true);
621  m_secondAnnounceTimer = startTimer(10 * 60 * 1000);
622  }
623  // refresh the cache
624  else if (event->timerId() == m_refreshTimer)
625  {
626  Refresh();
627  }
628  }
629  }
630 
631  return QObject::event(Event);
632 }
633 
634 void TorcSSDP::Read(void)
635 {
636  QUdpSocket *socket = dynamic_cast<QUdpSocket*>(sender());
637  while (socket && socket->hasPendingDatagrams())
638  {
639  QHostAddress address;
640  quint16 port;
641  QByteArray datagram;
642  datagram.resize(socket->pendingDatagramSize());
643  socket->readDatagram(datagram.data(), datagram.size(), &address, &port);
644 
645  // filter out our own messages.
646  // N.B. this also filters out messages from other instances running on the same device (with the same ip)
647  if ((m_ipv4MulticastSocket && (port == m_ipv4MulticastSocket->localPort())) || (m_ipv6LinkMulticastSocket && (port == m_ipv6LinkMulticastSocket->localPort())))
648  if (m_addressess.contains(address))
649  continue;
650 
651  // use a QString for greater flexibility in splitting and searching text
652  QString data(datagram);
653  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Raw datagram:\r\n%1").arg(data));
654 
655  // split data into lines
656  QStringList lines = data.split(QStringLiteral("\r\n"), QString::SkipEmptyParts);
657  if (lines.isEmpty())
658  continue;
659 
660  QMap<QString,QString> headers;
661 
662  // pull out type
663  QString type = lines.takeFirst().trimmed();
664 
665  // process headers
666  while (!lines.isEmpty())
667  {
668  QString header = lines.takeFirst();
669  int index = header.indexOf(':');
670  if (index > -1)
671  headers.insert(header.left(index).trimmed().toLower(), header.mid(index + 1).trimmed());
672  }
673 
674  if (type.startsWith(QStringLiteral("HTTP"), Qt::CaseInsensitive))
675  {
676  // response
677  if (headers.contains(QStringLiteral("cache-control")))
678  {
679  qint64 expires = GetExpiryTime(headers.value(QStringLiteral("cache-control")));
680  if (expires > 0)
681  ProcessDevice(headers, expires, true/*add*/);
682  }
683  }
684  else if (type.startsWith(QStringLiteral("NOTIFY"), Qt::CaseInsensitive))
685  {
686  // notfification ssdp:alive or ssdp:bye
687  if (headers.contains(QStringLiteral("nts")))
688  {
689  bool add = headers.value(QStringLiteral("nts")) != QStringLiteral("ssdp:byebye");
690 
691  if (add)
692  {
693  if (headers.contains(QStringLiteral("cache-control")))
694  {
695  qint64 expires = GetExpiryTime(headers.value(QStringLiteral("cache-control")));
696  if (expires > 0)
697  ProcessDevice(headers, expires, true/*add*/);
698  }
699  }
700  else
701  {
702  ProcessDevice(headers, 1, false/*remove*/);
703  }
704  }
705  }
706  else if (type.startsWith(QStringLiteral("M-SEARCH"), Qt::CaseInsensitive))
707  {
708  // check MAN for ssdp:discover
709  if (headers.value(QStringLiteral("man")).contains(QStringLiteral("ssdp:discover"), Qt::CaseInsensitive))
710  {
711  // already have host address from datagram
712  // need MX value in seconds
713  // lazy - should be present and in the range 1-120 - but limit to 5
714  int mx = qBound(1, headers.value(QStringLiteral("mx")).toInt(), 5);
715 
716  // check the target
717  TorcSSDPSearchResponse::ResponseTypes types(TorcSSDPSearchResponse::None);
718  QString st = headers.value(QStringLiteral("st"));
719  if (st == QStringLiteral("ssdp:all"))
720  {
724  }
725  else if (st == QStringLiteral("upnp:rootdevice"))
726  {
728  }
729  else if (st.startsWith(QStringLiteral("uuid:")))
730  {
731  if (st.mid(5) == TORC_ROOT_UPNP_DEVICE)
733  }
734  else if (st == TORC_ROOT_UPNP_DEVICE)
735  {
737  }
738 
739  if (types > TorcSSDPSearchResponse::None)
740  {
741  qint64 firstrandom = qrand() % ((mx * 1000) >> 1);
742  qint64 secondrandom = qrand() % (mx * 1000);
743  m_responseQueue.insertMulti(QDateTime::currentMSecsSinceEpoch() + firstrandom, TorcSSDPSearchResponse(address, types, port));
744  m_responseQueue.insertMulti(QDateTime::currentMSecsSinceEpoch() + secondrandom, TorcSSDPSearchResponse(address, types, port));
745 
746  // if the first response is due before the response timer times out, reset it.
747  firstrandom = qMin(firstrandom, secondrandom);
748  if (firstrandom < m_responseTimer.remainingTime())
749  m_responseTimer.start(firstrandom + 1);
750  }
751  }
752  else
753  {
754  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("Unknown MAN value in search request"));
755  }
756  }
757  }
758 }
759 
761 {
762  qint64 timenow = QDateTime::currentMSecsSinceEpoch();
763 
764  bool finished = false;
765  while (!finished && !m_responseQueue.isEmpty())
766  {
767  qint64 time = m_responseQueue.firstKey();
768  if (time <= timenow)
769  {
770  const TorcSSDPSearchResponse response = m_responseQueue.take(time);
771  ProcessResponse(response);
772  }
773  else
774  {
775  finished = true;
776  }
777  }
778 
779  qint64 nextcheck = 5000;
780  if (!m_responseQueue.isEmpty())
781  nextcheck = m_responseQueue.firstKey() - timenow;
782  m_responseTimer.start(nextcheck);
783 }
784 
785 void TorcSSDP::ProcessResponse(const TorcSSDPSearchResponse &Response)
786 {
787  if (!m_announceOptions.port)
788  {
789  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Cannot send response to %1 - no port set, not advertising!")
790  .arg(Response.m_responseAddress.toString()));
791  return;
792  }
793 
794  /* As per notify (NT -> ST)
795  * Root - ST: upnp:rootdevice USN: uuid:device-UUID::upnp:rootdevice
796  * UUID - ST: uuid:device-UUID USN: uuid:device-UUID
797  * Device - ST: urn:schemas-torcapp-org:device:TorcClient:1 USN: uuid:device-UUID::urn:schemas-torcapp-org:device:TorcClient:1
798  */
799 
800  // TODO this duplicates format from TorcHTTPRequest
801  static const QString dateformat(QStringLiteral("ddd, dd MMM yyyy HH:mm:ss 'GMT'"));
802  static const QString reply(QStringLiteral(
803  "HTTP/1.1 200 OK\r\n"
804  "CACHE-CONTROL: max-age=1800\r\n"
805  "DATE: %1\r\n"
806  "EXT:\r\n"
807  "LOCATION: http%2://%3:%4/upnp/description\r\n"
808  "SERVER: %5\r\n"
809  "ST: %6\r\n"
810  "USN: %7\r\n"
811  "NAME: %8\r\n"
812  "APIVERSION: %9\r\n"
813  "STARTTIME: %10\r\n"
814  "PRIORITY: %11\r\n%12\r\n"));
815 
816  QString date = QDateTime::currentDateTime().toString(dateformat);
817  QString uuid = QStringLiteral("uuid:") + gLocalContext->GetUuid();
818  bool ipv6 = Response.m_responseAddress.protocol() == QAbstractSocket::IPv6Protocol;
819  if ((ipv6 && m_ipv6Address.isEmpty()) || (!ipv6 && m_ipv4Address.isEmpty()))
820  return;
821  QUdpSocket *socket = ipv6 ? m_ipv6UnicastSocket : m_ipv4UnicastSocket;
822  QString raddress = QStringLiteral("%1:%2").arg(Response.m_responseAddress.toString(), Response.m_port);
823  QString secure = m_announceOptions.secure ? QStringLiteral("s") : QStringLiteral("");
824  QString secure2 = m_announceOptions.secure ? QStringLiteral("SECURE: yes\r\n") : QStringLiteral("");
825  QString name = TorcHTTPServer::ServerDescription();
826  QString apiversion = TorcHTTPServices::GetVersion();
827  QString starttime = QString::number(gLocalContext->GetStartTime());
828  QString priority = QString::number(gLocalContext->GetPriority());
829  int sent;
830  QByteArray packet;
831  if (Response.m_responseTypes.testFlag(TorcSSDPSearchResponse::Root))
832  {
833  packet = QString(reply).arg(date, secure, ipv6 ? m_ipv6Address : m_ipv4Address).arg(m_announceOptions.port)
834  .arg(m_serverString, QStringLiteral("upnp:rootdevice"), QStringLiteral("%1::upnp:rootdevice").arg(uuid), name, apiversion, starttime, priority, secure2).toLocal8Bit();
835  sent = socket->writeDatagram(packet, Response.m_responseAddress, Response.m_port);
836  if (sent != packet.size())
837  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Error sending SSDP root search response to %1 (%2)").arg(raddress, socket->errorString()));
838  else
839  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("Sent SSDP root search response to %1").arg(raddress));
840  }
841 
842  if (Response.m_responseTypes.testFlag(TorcSSDPSearchResponse::UUID))
843  {
844  packet = QString(reply).arg(date, secure, ipv6 ? m_ipv6Address : m_ipv4Address).arg(m_announceOptions.port)
845  .arg(m_serverString, uuid, uuid, name, apiversion, starttime, priority, secure2).toLocal8Bit();
846  sent = socket->writeDatagram(packet, Response.m_responseAddress, Response.m_port);
847  if (sent != packet.size())
848  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Error sending SSDP UUID search response to %1 (%2)").arg(raddress, socket->errorString()));
849  else
850  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("Sent SSDP UUID search response to %1").arg(raddress));
851  }
852 
853  if (Response.m_responseTypes.testFlag(TorcSSDPSearchResponse::Device))
854  {
855  packet = QString(reply).arg(date, secure, ipv6 ? m_ipv6Address : m_ipv4Address).arg(m_announceOptions.port)
856  .arg(m_serverString, TORC_ROOT_UPNP_DEVICE, uuid + "::" + TORC_ROOT_UPNP_DEVICE, name, apiversion, starttime, priority, secure2).toLocal8Bit();
857  sent = socket->writeDatagram(packet, Response.m_responseAddress, Response.m_port);
858  if (sent != packet.size())
859  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Error sending SSDP Device search response to %1 (%2)").arg(raddress, socket->errorString()));
860  else
861  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("Sent SSDP UUID Device response to %1").arg(raddress));
862  }
863 }
864 
865 void TorcSSDP::Refresh(void)
866 {
867  if (!m_started)
868  return;
869 
870  qint64 now = QDateTime::currentMSecsSinceEpoch();
871 
872  // remove stale discovered devices (if still present, they should have notified
873  // a refresh)
874  QVector<TorcUPNPDescription> removed;
875  QMutableHashIterator<QString,TorcUPNPDescription> it(m_discoveredDevices);
876  while (it.hasNext())
877  {
878  it.next();
879  if (it.value().GetExpiry() < now)
880  {
881  removed << it.value();
882  it.remove();
883  }
884  }
885 
886  // notify removal
887  QVector<TorcUPNPDescription>::const_iterator it2 = removed.constBegin();
888  for ( ; it2 != removed.constEnd(); ++it2)
889  {
890  QVariantMap data;
891  data.insert(TORC_USN, (*it2).GetUSN());
893  gLocalContext->Notify(event);
894  }
895 
896 
897  LOG(VB_NETWORK, LOG_INFO, QStringLiteral("Removed %1 stale cache entries").arg(removed.size()));
898 }
899 
900 void TorcSSDP::ProcessDevice(const QMap<QString,QString> &Headers, qint64 Expires, bool Add)
901 {
902  if (!Headers.contains(TORC_USN) || !Headers.contains(QStringLiteral("location")) || Expires < 1)
903  return;
904 
905  QString USN = Headers.value(TORC_USN);
906  QString location = Headers.value(QStringLiteral("location"));
907 
908  if (!USN.contains(TORC_ROOT_UPNP_DEVICE))
909  return;
910 
911  bool notify = true;
912 
913  if (m_discoveredDevices.contains(USN))
914  {
915  // is this just an expiry update?
916  if (Add)
917  {
918  notify = false;
919  (void)m_discoveredDevices.remove(USN);
920  m_discoveredDevices.insert(USN, TorcUPNPDescription(USN, location, Expires));
921  }
922  else
923  {
924  (void)m_discoveredDevices.remove(USN);
925  }
926  }
927  else if (Add)
928  {
929  m_discoveredDevices.insert(USN, TorcUPNPDescription(USN, location, Expires));
930  }
931 
932  // update interested parties
933  if (notify)
934  {
935  QVariantMap data;
936  data.insert(TORC_USN, USN);
937  data.insert(TORC_ADDRESS, location);
938  data.insert(TORC_NAME, Headers.value(TORC_NAME));
939  data.insert(TORC_APIVERSION, Headers.value(TORC_APIVERSION));
940  data.insert(TORC_STARTTIME, Headers.value(TORC_STARTTIME));
941  data.insert(TORC_PRIORITY, Headers.value(TORC_PRIORITY));
942  if (Headers.contains(TORC_SECURE))
943  data.insert(TORC_SECURE, TORC_YES);
945  gLocalContext->Notify(event);
946  }
947 }
948 
949 void TorcSSDP::StartSearch(void)
950 {
951  gSSDPLock->lock();
953  gSSDPLock->unlock();
954 
955  if (m_searching && newoptions == m_searchOptions)
956  {
957  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("SSDP search options unchanged - not restarting"));
958  return;
959  }
960 
961  StopSearch();
962 
963  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Starting SSDP search for %1 devices").arg(TORC_ROOT_UPNP_DEVICE));
964  m_searchOptions = newoptions;
965 
966  // schedule first search almost immediately - this is evented to ensure subsequent searches are scheduled
967  m_firstSearchTimer = startTimer(1, Qt::CoarseTimer);
968  // and schedule another search for after the MX time in our first request (3)
969  m_secondSearchTimer = startTimer(5000, Qt::CoarseTimer);
970 
971  m_searchDebugged = false;
972  m_searching = true;
973 }
974 
975 void TorcSSDP::StopSearch(void)
976 {
977  if (m_searching)
978  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Stopping SSDP search for %1 devices").arg(TORC_ROOT_UPNP_DEVICE));
979 
980  if (m_firstSearchTimer)
981  {
982  killTimer(m_firstSearchTimer);
983  m_firstSearchTimer = 0;
984  }
985 
986  if (m_secondSearchTimer)
987  {
988  killTimer(m_secondSearchTimer);
989  m_secondSearchTimer = 0;
990  }
991 
992  m_searching = false;
993 }
994 
995 qint64 TorcSSDP::GetExpiryTime(const QString &Expires)
996 {
997  // NB this ignores the date header - just assume it is correct
998  int index = Expires.indexOf(QStringLiteral("max-age"), 0, Qt::CaseInsensitive);
999  if (index > -1)
1000  {
1001  int index2 = Expires.indexOf('=', index);
1002  if (index2 > -1)
1003  {
1004  int seconds = Expires.midRef(index2 + 1).toInt();
1005  return QDateTime::currentMSecsSinceEpoch() + 1000 * seconds;
1006  }
1007  }
1008 
1009  return -1;
1010 }
1011 
1012 TorcSSDPThread::TorcSSDPThread() : TorcQThread(QStringLiteral("SSDP"))
1013 {
1014 }
1015 
1017 {
1018  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("SSDP thread starting"));
1019  TorcSSDP::Create();
1020 }
1021 
1023 {
1024  TorcSSDP::Create(true/*destroy*/);
1025  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("SSDP thread stopping"));
1026 }
#define TORC_PRIORITY
Definition: torcupnp.h:19
void SendAnnounce(bool IsIPv6, bool Alive)
Definition: torcssdp.cpp:502
#define TORC_IPV4_UDP_MULTICAST_ADDR
Definition: torcssdp.h:17
TorcLocalContext * gLocalContext
static TorcSSDP * gSSDP
Definition: torcssdp.h:57
static QMutex * gSSDPLock
Definition: torcssdp.h:58
QHostAddress m_responseAddress
Definition: torcssdp.h:40
#define TORC_IPV6_UDP_MULTICAST_URL
Definition: torcssdp.h:21
void CancelAnnouncePriv(void)
Definition: torcssdp.cpp:446
void SendSearch(void)
Definition: torcssdp.cpp:380
#define TORC_STARTTIME
Definition: torcupnp.h:20
void AnnouncePriv(void)
Definition: torcssdp.cpp:439
static void CancelAnnounce(void)
Definition: torcssdp.cpp:305
static bool IsGlobal(const QHostAddress &Address)
QString GetUuid(void) const
#define TORC_USN
Definition: torcupnp.h:23
static bool IsAvailable(void)
Definition: torcnetwork.cpp:46
void Read(void)
Definition: torcssdp.cpp:634
#define TORC_APIVERSION
Definition: torcupnp.h:18
void SearchPriv(void)
Definition: torcssdp.cpp:427
#define TORC_IPV4_UDP_MULTICAST_URL
Definition: torcssdp.h:18
static TorcSSDP * Create(bool Destroy=false)
Definition: torcssdp.cpp:314
#define TORC_YES
Definition: torcupnp.h:24
ResponseTypes m_responseTypes
Definition: torcssdp.h:41
#define TORC_SECURE
Definition: torcupnp.h:22
static void CancelSearch(void)
Stop searching for a UPnP device type.
Definition: torcssdp.cpp:283
static void Announce(TorcHTTPServer::Status Options)
Publish the device.
Definition: torcssdp.cpp:294
The public class for handling Simple Service Discovery Protocol searches and announcements.
Definition: torcssdp.h:45
static bool IsLinkLocal(const QHostAddress &Address)
static TorcHTTPServer::Status gAnnounceOptions
Definition: torcssdp.h:62
#define TORC_ADDRESS
Definition: torcupnp.h:21
static Type TorcEventType
Register TorcEventType with QEvent.
Definition: torcevent.h:19
static bool gAnnounceEnabled
Definition: torcssdp.h:61
#define IPv4
Definition: torcssdp.cpp:330
static void Search(TorcHTTPServer::Status Options)
Search for Torc UPnP device type.
Definition: torcssdp.cpp:269
void Finish(void) override
Definition: torcssdp.cpp:1022
static bool IsExternal(const QHostAddress &Address)
Returns true if the address is accessible from other devices.
#define TORC_ROOT_UPNP_DEVICE
Definition: torcupnp.h:7
TorcSSDP()
Definition: torcssdp.cpp:63
static bool gSearchEnabled
Definition: torcssdp.h:59
A general purpose event object.
Definition: torcevent.h:9
#define TORC_IPV6_UDP_MULTICAST_ADDR2
Definition: torcssdp.h:20
static QString GetVersion(void)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: torclogging.h:20
static TorcHTTPServer::Status gSearchOptions
Definition: torcssdp.h:60
void AddObserver(QObject *Observer)
brief Register the given object to receive events.
void Notify(const TorcEvent &Event)
Brief Send the given event to each registered listener/observer.
#define IPv6
Definition: torcssdp.cpp:331
bool event(QEvent *Event) override
Definition: torcssdp.cpp:566
void Start(void) override
Definition: torcssdp.cpp:1016
qint64 GetStartTime(void)
void ProcessResponses(void)
Definition: torcssdp.cpp:760
void RemoveObserver(QObject *Observer)
brief Deregister the given object.
static QString ServerDescription(void)
A Torc specific wrapper around QThread.
Definition: torcqthread.h:7
#define TORC_SSDP_UDP_MULTICAST_PORT
Definition: torcssdp.h:16
#define TORC_NAME
Definition: torcupnp.h:16
static bool IsLocal(const QHostAddress &Address)
void CancelSearchPriv(void)
Definition: torcssdp.cpp:434