2018-12-30 22:07:53 +01:00
# include <ssdp/SSDPDiscover.h>
2020-08-02 22:32:00 +02:00
# include <utils/Logger.h>
2020-07-12 20:27:56 +02:00
# include <utils/QStringUtils.h>
2020-06-28 23:05:32 +02:00
// Qt includes
2018-12-30 22:07:53 +01:00
# include <QUdpSocket>
# include <QUrl>
2020-07-12 20:27:56 +02:00
# include <QRegularExpression>
2020-08-02 22:32:00 +02:00
# include <QJsonArray>
2020-07-12 20:27:56 +02:00
# include <QJsonObject>
2020-08-02 22:32:00 +02:00
# include <QHostInfo>
2018-12-30 22:07:53 +01:00
2020-07-12 20:27:56 +02:00
// Constants
namespace {
2018-12-30 22:07:53 +01:00
// as per upnp spec 1.1, section 1.2.2.
2020-07-12 20:27:56 +02:00
const QString UPNP_DISCOVER_MESSAGE = " M-SEARCH * HTTP/1.1 \r \n "
" HOST: %1:%2 \r \n "
2021-11-16 18:12:56 +01:00
" MAN: \" ssdp:discover \" \r \n "
2020-07-12 20:27:56 +02:00
" MX: %3 \r \n "
" ST: %4 \r \n "
2021-11-16 18:12:56 +01:00
" \r \n " ;
2020-07-12 20:27:56 +02:00
} //End of constants
2018-12-30 22:07:53 +01:00
SSDPDiscover : : SSDPDiscover ( QObject * parent )
: QObject ( parent )
2020-07-12 20:27:56 +02:00
, _log ( Logger : : getInstance ( " SSDPDISCOVER " ) )
, _udpSocket ( new QUdpSocket ( this ) )
, _ssdpAddr ( DEFAULT_SEARCH_ADDRESS )
, _ssdpPort ( DEFAULT_SEARCH_PORT )
, _ssdpMaxWaitResponseTime ( 1 )
, _ssdpTimeout ( DEFAULT_SSDP_TIMEOUT . count ( ) )
, _filter ( DEFAULT_FILTER )
, _filterHeader ( DEFAULT_FILTER_HEADER )
, _regExFilter ( _filter )
, _skipDupKeys ( false )
2018-12-30 22:07:53 +01:00
{
}
void SSDPDiscover : : searchForService ( const QString & st )
{
_searchTarget = st ;
_usnList . clear ( ) ;
// setup socket
connect ( _udpSocket , & QUdpSocket : : readyRead , this , & SSDPDiscover : : readPendingDatagrams , Qt : : UniqueConnection ) ;
sendSearch ( st ) ;
}
2020-08-08 23:12:43 +02:00
QString SSDPDiscover : : getFirstService ( const searchType & type , const QString & st , int timeout_ms )
2018-12-30 22:07:53 +01:00
{
_searchTarget = st ;
2020-07-12 20:27:56 +02:00
_services . clear ( ) ;
Debug ( _log , " Search for Service [%s], address [%s], port [%d] " , QSTRING_CSTR ( _searchTarget ) , QSTRING_CSTR ( _ssdpAddr . toString ( ) ) , _ssdpPort ) ;
2018-12-30 22:07:53 +01:00
// search
2020-06-28 23:05:32 +02:00
sendSearch ( _searchTarget ) ;
2018-12-30 22:07:53 +01:00
2020-04-05 19:41:02 +02:00
if ( _udpSocket - > waitForReadyRead ( timeout_ms ) )
2018-12-30 22:07:53 +01:00
{
2020-04-05 19:41:02 +02:00
while ( _udpSocket - > waitForReadyRead ( 500 ) )
{
QByteArray datagram ;
while ( _udpSocket - > hasPendingDatagrams ( ) )
{
datagram . resize ( _udpSocket - > pendingDatagramSize ( ) ) ;
QHostAddress sender ;
quint16 senderPort ;
2019-04-08 23:13:11 +02:00
2020-04-05 19:41:02 +02:00
_udpSocket - > readDatagram ( datagram . data ( ) , datagram . size ( ) , & sender , & senderPort ) ;
2018-12-30 22:07:53 +01:00
2020-04-05 19:41:02 +02:00
QString data ( datagram ) ;
2018-12-30 22:07:53 +01:00
2020-06-28 23:05:32 +02:00
//Debug(_log, "_data: [%s]", QSTRING_CSTR(data));
2018-12-30 22:07:53 +01:00
2020-04-05 19:41:02 +02:00
QMap < QString , QString > headers ;
QString address ;
// parse request
2020-06-28 23:05:32 +02:00
2020-07-12 20:27:56 +02:00
QStringList entries = QStringUtils : : split ( data , " \n " , QStringUtils : : SplitBehavior : : SkipEmptyParts ) ;
2020-04-05 19:41:02 +02:00
for ( auto entry : entries )
{
// http header parse skip
if ( entry . contains ( " HTTP/1.1 " ) )
continue ;
2018-12-30 22:07:53 +01:00
2020-04-05 19:41:02 +02:00
// split into key:vale, be aware that value field may contain also a ":"
entry = entry . simplified ( ) ;
int pos = entry . indexOf ( " : " ) ;
if ( pos = = - 1 )
continue ;
2018-12-30 22:07:53 +01:00
2020-04-05 19:41:02 +02:00
headers [ entry . left ( pos ) . trimmed ( ) . toLower ( ) ] = entry . mid ( pos + 1 ) . trimmed ( ) ;
}
2018-12-30 22:07:53 +01:00
2020-04-05 19:41:02 +02:00
// verify ssdp spec
if ( ! headers . contains ( " st " ) )
2018-12-30 22:07:53 +01:00
continue ;
2020-04-05 19:41:02 +02:00
// usn duplicates
if ( _usnList . contains ( headers . value ( " usn " ) ) )
2019-08-17 09:44:57 +02:00
continue ;
2020-04-05 19:41:02 +02:00
if ( headers . value ( " st " ) = = _searchTarget )
2019-08-17 09:44:57 +02:00
{
2020-04-05 19:41:02 +02:00
_usnList < < headers . value ( " usn " ) ;
QUrl url ( headers . value ( " location " ) ) ;
//Debug(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st")));
2020-06-28 23:05:32 +02:00
if ( type = = searchType : : STY_WEBSERVER )
2020-04-05 19:41:02 +02:00
{
Debug ( _log , " Found service [%s] at: %s:%d " , QSTRING_CSTR ( st ) , QSTRING_CSTR ( url . host ( ) ) , url . port ( ) ) ;
return url . host ( ) + " : " + QString : : number ( url . port ( ) ) ;
}
2020-06-28 23:05:32 +02:00
else if ( type = = searchType : : STY_FLATBUFSERVER )
2020-04-05 19:41:02 +02:00
{
const QString fbsport = headers . value ( " hyperion-fbs-port " ) ;
if ( fbsport . isEmpty ( ) )
{
continue ;
}
else
{
Debug ( _log , " Found service [%s] at: %s:%s " , QSTRING_CSTR ( st ) , QSTRING_CSTR ( url . host ( ) ) , QSTRING_CSTR ( fbsport ) ) ;
return url . host ( ) + " : " + fbsport ;
}
}
2020-06-28 23:05:32 +02:00
else if ( type = = searchType : : STY_JSONSERVER )
2020-04-05 19:41:02 +02:00
{
const QString jssport = headers . value ( " hyperion-jss-port " ) ;
if ( jssport . isEmpty ( ) )
{
continue ;
}
else
{
Debug ( _log , " Found service at: %s:%s " , QSTRING_CSTR ( url . host ( ) ) , QSTRING_CSTR ( jssport ) ) ;
return url . host ( ) + " : " + jssport ;
}
}
2019-08-17 09:44:57 +02:00
}
}
2018-12-30 22:07:53 +01:00
}
}
2019-09-14 22:54:41 +02:00
Debug ( _log , " Search timeout, service [%s] not found " , QSTRING_CSTR ( st ) ) ;
2018-12-30 22:07:53 +01:00
return QString ( ) ;
}
void SSDPDiscover : : readPendingDatagrams ( )
{
while ( _udpSocket - > hasPendingDatagrams ( ) ) {
QByteArray datagram ;
datagram . resize ( _udpSocket - > pendingDatagramSize ( ) ) ;
QHostAddress sender ;
quint16 senderPort ;
_udpSocket - > readDatagram ( datagram . data ( ) , datagram . size ( ) , & sender , & senderPort ) ;
QString data ( datagram ) ;
QMap < QString , QString > headers ;
// parse request
2020-07-12 20:27:56 +02:00
QStringList entries = QStringUtils : : split ( data , " \n " , QStringUtils : : SplitBehavior : : SkipEmptyParts ) ;
2018-12-30 22:07:53 +01:00
for ( auto entry : entries )
{
// http header parse skip
if ( entry . contains ( " HTTP/1.1 " ) )
continue ;
2020-06-28 23:19:06 +02:00
// split into key:value, be aware that value field may contain also a ":"
2018-12-30 22:07:53 +01:00
entry = entry . simplified ( ) ;
int pos = entry . indexOf ( " : " ) ;
if ( pos = = - 1 )
continue ;
2020-06-28 23:19:06 +02:00
const QString key = entry . left ( pos ) . trimmed ( ) . toLower ( ) ;
const QString value = entry . mid ( pos + 1 ) . trimmed ( ) ;
headers [ key ] = value ;
2018-12-30 22:07:53 +01:00
}
// verify ssdp spec
if ( ! headers . contains ( " st " ) )
continue ;
// usn duplicates
if ( _usnList . contains ( headers . value ( " usn " ) ) )
continue ;
if ( headers . value ( " st " ) = = _searchTarget )
{
_usnList < < headers . value ( " usn " ) ;
2019-09-14 22:54:41 +02:00
//Debug(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st")));
2018-12-30 22:07:53 +01:00
QUrl url ( headers . value ( " location " ) ) ;
2020-06-28 23:19:06 +02:00
emit newService ( url . host ( ) + " : " + QString : : number ( url . port ( ) ) ) ;
2018-12-30 22:07:53 +01:00
}
}
}
2020-07-12 20:27:56 +02:00
int SSDPDiscover : : discoverServices ( const QString & searchTarget , const QString & key )
{
_searchTarget = searchTarget ;
int rc = - 1 ;
Debug ( _log , " Search for Service [%s], address [%s], port [%d] " , QSTRING_CSTR ( _searchTarget ) , QSTRING_CSTR ( _ssdpAddr . toString ( ) ) , _ssdpPort ) ;
_services . clear ( ) ;
// search
sendSearch ( _searchTarget ) ;
if ( _udpSocket - > waitForReadyRead ( _ssdpTimeout ) )
{
while ( _udpSocket - > waitForReadyRead ( 500 ) )
{
QByteArray datagram ;
while ( _udpSocket - > hasPendingDatagrams ( ) )
{
datagram . resize ( _udpSocket - > pendingDatagramSize ( ) ) ;
QHostAddress sender ;
quint16 senderPort ;
_udpSocket - > readDatagram ( datagram . data ( ) , datagram . size ( ) , & sender , & senderPort ) ;
QString data ( datagram ) ;
//Debug(_log, "_data: [%s]", QSTRING_CSTR(data));
QMap < QString , QString > headers ;
// parse request
QStringList entries = QStringUtils : : split ( data , " \n " , QStringUtils : : SplitBehavior : : SkipEmptyParts ) ;
for ( auto entry : entries )
{
// http header parse skip
if ( entry . contains ( " HTTP/1.1 " ) )
continue ;
// split into key:vale, be aware that value field may contain also a ":"
entry = entry . simplified ( ) ;
int pos = entry . indexOf ( " : " ) ;
if ( pos = = - 1 )
continue ;
headers [ entry . left ( pos ) . trimmed ( ) . toUpper ( ) ] = entry . mid ( pos + 1 ) . trimmed ( ) ;
}
QRegularExpressionMatch match = _regExFilter . match ( headers [ _filterHeader ] ) ;
if ( match . hasMatch ( ) )
{
Debug ( _log , " Found target [%s], plus record [%s] matches [%s:%s] " , QSTRING_CSTR ( _searchTarget ) , QSTRING_CSTR ( headers [ _filterHeader ] ) , QSTRING_CSTR ( _filterHeader ) , QSTRING_CSTR ( _filter ) ) ;
//Debug(_log, "_data: [%s]", QSTRING_CSTR(data));
QString mapKey = headers [ key ] ;
SSDPService service ;
service . cacheControl = headers [ " CACHE-CONTROL " ] ;
service . location = QUrl ( headers [ " LOCATION " ] ) ;
service . server = headers [ " SERVER " ] ;
service . searchTarget = headers [ " ST " ] ;
service . uniqueServiceName = headers [ " USN " ] ;
headers . remove ( " CACHE-CONTROL " ) ;
headers . remove ( " LOCATION " ) ;
headers . remove ( " SERVER " ) ;
headers . remove ( " ST " ) ;
headers . remove ( " USN " ) ;
service . otherHeaders = headers ;
if ( _skipDupKeys )
{
2020-08-08 12:54:30 +02:00
_services . replace ( mapKey , service ) ;
2020-07-12 20:27:56 +02:00
}
else
{
2020-08-08 12:54:30 +02:00
_services . insert ( mapKey , service ) ;
2020-07-12 20:27:56 +02:00
}
}
}
}
}
_udpSocket - > close ( ) ;
if ( _services . empty ( ) )
{
Debug ( _log , " Search target [%s], no record(s) matching [%s:%s] " , QSTRING_CSTR ( _searchTarget ) , QSTRING_CSTR ( _filterHeader ) , QSTRING_CSTR ( _filter ) ) ;
rc = 0 ;
}
else
{
rc = _services . size ( ) ;
Debug ( _log , " [%d] service record(s) found " , rc ) ;
}
return rc ;
}
2020-08-08 23:12:43 +02:00
QJsonArray SSDPDiscover : : getServicesDiscoveredJson ( ) const
2020-07-12 20:27:56 +02:00
{
QJsonArray result ;
2021-11-16 18:12:56 +01:00
QMultiMap < QString , SSDPService > : : const_iterator i ;
2020-07-12 20:27:56 +02:00
for ( i = _services . begin ( ) ; i ! = _services . end ( ) ; + + i )
{
//Debug(_log, "Device discovered at [%s]", QSTRING_CSTR( i.key() ));
QJsonObject obj ;
obj . insert ( " id " , i . key ( ) ) ;
obj . insert ( " cache-control " , i . value ( ) . cacheControl ) ;
obj . insert ( " location " , i . value ( ) . location . toString ( ) ) ;
obj . insert ( " server " , i . value ( ) . server ) ;
obj . insert ( " st " , i . value ( ) . searchTarget ) ;
obj . insert ( " usn " , i . value ( ) . uniqueServiceName ) ;
QUrl url ( i . value ( ) . location ) ;
2020-11-14 16:35:55 +01:00
QString ipAddress = url . host ( ) ;
obj . insert ( " ip " , ipAddress ) ;
2020-07-12 20:27:56 +02:00
obj . insert ( " port " , url . port ( ) ) ;
QHostInfo hostInfo = QHostInfo : : fromName ( url . host ( ) ) ;
2020-11-14 16:35:55 +01:00
if ( hostInfo . error ( ) = = QHostInfo : : NoError )
2020-07-12 20:27:56 +02:00
{
QString hostname = hostInfo . hostName ( ) ;
2020-11-14 16:35:55 +01:00
if ( ! QHostInfo : : localDomainName ( ) . isEmpty ( ) )
2020-07-12 20:27:56 +02:00
{
2020-11-14 16:35:55 +01:00
obj . insert ( " hostname " , hostname . remove ( " . " + QHostInfo : : localDomainName ( ) ) ) ;
obj . insert ( " domain " , QHostInfo : : localDomainName ( ) ) ;
2020-07-12 20:27:56 +02:00
}
else
{
2020-11-14 16:35:55 +01:00
if ( hostname . startsWith ( ipAddress ) )
{
obj . insert ( " hostname " , ipAddress ) ;
QString domain = hostname . remove ( ipAddress ) ;
if ( domain . at ( 0 ) = = ' . ' )
{
domain . remove ( 0 , 1 ) ;
}
obj . insert ( " domain " , domain ) ;
}
else
{
int domainPos = hostname . indexOf ( ' . ' ) ;
obj . insert ( " hostname " , hostname . left ( domainPos ) ) ;
obj . insert ( " domain " , hostname . mid ( domainPos + 1 ) ) ;
}
2020-07-12 20:27:56 +02:00
}
}
QJsonObject objOther ;
2020-08-08 23:12:43 +02:00
QMap < QString , QString > : : const_iterator o ;
2020-07-12 20:27:56 +02:00
for ( o = i . value ( ) . otherHeaders . begin ( ) ; o ! = i . value ( ) . otherHeaders . end ( ) ; + + o )
{
objOther . insert ( o . key ( ) . toLower ( ) , o . value ( ) ) ;
}
obj . insert ( " other " , objOther ) ;
result < < obj ;
}
//Debug(_log, "result: [%s]", QString(QJsonDocument(result).toJson(QJsonDocument::Compact)).toUtf8().constData() );
return result ;
}
2018-12-30 22:07:53 +01:00
void SSDPDiscover : : sendSearch ( const QString & st )
{
2020-07-12 20:27:56 +02:00
const QString msg = QString ( UPNP_DISCOVER_MESSAGE ) . arg ( _ssdpAddr . toString ( ) ) . arg ( _ssdpPort ) . arg ( _ssdpMaxWaitResponseTime ) . arg ( st ) ;
2018-12-30 22:07:53 +01:00
2020-07-12 20:27:56 +02:00
//Debug(_log,"Search request: [%s]", QSTRING_CSTR(msg));
_udpSocket - > writeDatagram ( msg . toUtf8 ( ) , _ssdpAddr , _ssdpPort ) ;
}
bool SSDPDiscover : : setSearchFilter ( const QString & filter , const QString & filterHeader )
{
bool rc = true ;
QRegularExpression regEx ( filter ) ;
if ( ! regEx . isValid ( ) ) {
QString errorString = regEx . errorString ( ) ;
int errorOffset = regEx . patternErrorOffset ( ) ;
Error ( _log , " Filtering regular expression [%s] error [%d]:[%s] " , QSTRING_CSTR ( filter ) , errorOffset , QSTRING_CSTR ( errorString ) ) ;
rc = false ;
}
else
{
_filter = filter ;
_filterHeader = filterHeader . toUpper ( ) ;
_regExFilter = regEx ;
}
return rc ;
2018-12-30 22:07:53 +01:00
}