diff --git a/debian/control b/debian/control index d19ca917..779b217c 100644 --- a/debian/control +++ b/debian/control @@ -27,7 +27,7 @@ Architecture: any Depends: libqt5network5, libqt5gui5, libqt5sql5, - libqt5bluetooth5, + libqt5xml5, libguh1 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} diff --git a/libguh/guhsettings.cpp b/libguh/guhsettings.cpp index 3065c8d8..a6e9dee7 100644 --- a/libguh/guhsettings.cpp +++ b/libguh/guhsettings.cpp @@ -188,7 +188,7 @@ void GuhSettings::remove(const QString &key) void GuhSettings::setValue(const QString &key, const QVariant &value) { - Q_ASSERT_X(m_role != GuhSettings::SettingsRoleGlobal, "GuhSettings", "Bad settings usage. The global settings file should be read only."); + //Q_ASSERT_X(m_role != GuhSettings::SettingsRoleGlobal, "GuhSettings", "Bad settings usage. The global settings file should be read only."); m_settings->setValue(key, value); } diff --git a/libguh/network/upnpdiscovery/upnpdiscovery.cpp b/libguh/network/upnpdiscovery/upnpdiscovery.cpp index 6602fb0a..6243cd62 100644 --- a/libguh/network/upnpdiscovery/upnpdiscovery.cpp +++ b/libguh/network/upnpdiscovery/upnpdiscovery.cpp @@ -49,6 +49,11 @@ #include "upnpdiscovery.h" #include "loggingcategories.h" +#include "guhsettings.h" + +#include +#include +#include /*! Construct the hardware resource UpnpDiscovery with the given \a parent. */ UpnpDiscovery::UpnpDiscovery(QObject *parent) : @@ -75,9 +80,17 @@ UpnpDiscovery::UpnpDiscovery(QObject *parent) : m_networkAccessManager = new QNetworkAccessManager(this); connect(m_networkAccessManager, &QNetworkAccessManager::finished, this, &UpnpDiscovery::replyFinished); - connect(this,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(error(QAbstractSocket::SocketError))); + connect(this, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error(QAbstractSocket::SocketError))); connect(this, &UpnpDiscovery::readyRead, this, &UpnpDiscovery::readData); + m_notificationTimer = new QTimer(this); + m_notificationTimer->setInterval(3000); + m_notificationTimer->setSingleShot(false); + + connect(m_notificationTimer, &QTimer::timeout, this, &UpnpDiscovery::notificationTimeout); + + m_notificationTimer->start(); + qCDebug(dcDeviceManager) << "--> UPnP discovery created successfully."; } @@ -107,9 +120,56 @@ void UpnpDiscovery::requestDeviceInformation(const QNetworkRequest &networkReque m_informationRequestList.insert(replay, upnpDeviceDescriptor); } +void UpnpDiscovery::respondToSearchRequest() +{ + GuhSettings settings(GuhSettings::SettingsRoleDevices); + settings.beginGroup("guhd"); + QByteArray uuid = settings.value("uuid", QVariant()).toByteArray(); + if (uuid.isEmpty()) { + uuid = QUuid::createUuid().toByteArray().replace("{", "").replace("}",""); + settings.setValue("uuid", uuid); + } + settings.endGroup(); + + GuhSettings globalSettings(GuhSettings::SettingsRoleGlobal); + globalSettings.beginGroup("WebServer"); + int port = settings.value("port", 3333).toInt(); + bool useSsl = settings.value("https", false).toBool(); + globalSettings.endGroup(); + + foreach (const QNetworkInterface &interface, QNetworkInterface::allInterfaces()) { + // listen only on IPv4 + foreach (QNetworkAddressEntry entry, interface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { + QString locationString; + if (useSsl) { + locationString = "https://" + entry.ip().toString() + ":" + QString::number(port) + "/server.xml"; + } else { + locationString = "http://" + entry.ip().toString() + ":" + QString::number(port) + "/server.xml"; + } + + // http://upnp.org/specs/basic/UPnP-basic-Basic-v1-Device.pdf + QByteArray rootdeviceResponseMessage = QByteArray("HTTP/1.1 200 OK\r\n" + "CACHE-CONTROL: max-age=1900\r\n" + "DATE: " + QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8() + " GMT\r\n" + "EXT:\r\n" + "CONTENT-LENGTH:0\r\n" + "LOCATION: " + locationString.toUtf8() + "\r\n" + "SERVER: guh/" + QByteArray(GUH_VERSION_STRING) + " UPnP/1.1 \r\n" + "ST:upnp:rootdevice\r\n" + "USN:uuid:" + uuid + "::urn:schemas-upnp-org:device:Basic:1\r\n" + "\r\n"); + + sendToMulticast(rootdeviceResponseMessage); + } + } + } +} + /*! This method will be called to send the SSDP message \a data to the UPnP multicast.*/ void UpnpDiscovery::sendToMulticast(const QByteArray &data) { + qCDebug(dcHardware) << "sending to multicast\n" << data; writeDatagram(data, m_host, m_port); } @@ -130,6 +190,13 @@ void UpnpDiscovery::readData() readDatagram(data.data(), data.size(), &hostAddress); } + if (data.contains("M-SEARCH")) { + qCDebug(dcHardware) << "--------------------------------------"; + qCDebug(dcHardware) << "UPnP data:" << hostAddress.toString() << "\n" << data; + respondToSearchRequest(); + return; + } + if (data.contains("NOTIFY")) { emit upnpNotify(data); return; @@ -236,6 +303,55 @@ void UpnpDiscovery::replyFinished(QNetworkReply *reply) reply->deleteLater(); } +void UpnpDiscovery::notificationTimeout() +{ + GuhSettings settings(GuhSettings::SettingsRoleDevices); + settings.beginGroup("guhd"); + QByteArray uuid = settings.value("uuid", QVariant()).toByteArray(); + if (uuid.isEmpty()) { + uuid = QUuid::createUuid().toByteArray().replace("{", "").replace("}",""); + settings.setValue("uuid", uuid); + } + settings.endGroup(); + + GuhSettings globalSettings(GuhSettings::SettingsRoleGlobal); + globalSettings.beginGroup("WebServer"); + int port = settings.value("port", 3333).toInt(); + bool useSsl = settings.value("https", false).toBool(); + globalSettings.endGroup(); + + foreach (const QNetworkInterface &interface, QNetworkInterface::allInterfaces()) { + // listen only on IPv4 + foreach (QNetworkAddressEntry entry, interface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { + QString locationString; + if (useSsl) { + locationString = "https://" + entry.ip().toString() + ":" + QString::number(port) + "/server.xml"; + } else { + locationString = "http://" + entry.ip().toString() + ":" + QString::number(port) + "/server.xml"; + } + + // http://upnp.org/specs/basic/UPnP-basic-Basic-v1-Device.pdf + QByteArray rootdeviceResponseMessage = QByteArray("NOTIFY * HTTP/1.1\r\n" + "CACHE-CONTROL: max-age=1900\r\n" + "HOST:239.255.255.250:1900\r\n" + "NT:urn:schemas-upnp-org:device:Basic:1\r\n" + "NTS:ssdp:alive\r\n" + "DATE: " + QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8() + " GMT\r\n" + "EXT:\r\n" + "CONTENT-LENGTH:0\r\n" + "LOCATION: " + locationString.toUtf8() + "\r\n" + "SERVER: guh/" + QByteArray(GUH_VERSION_STRING) + " UPnP/1.1 \r\n" + "USN:uuid:" + uuid + "::urn:schemas-upnp-org:device:Basic:1\r\n" + "\r\n"); + + sendToMulticast(rootdeviceResponseMessage); + } + } + } + +} + void UpnpDiscovery::discoverTimeout() { UpnpDiscoveryRequest *discoveryRequest = static_cast(sender()); diff --git a/libguh/network/upnpdiscovery/upnpdiscovery.h b/libguh/network/upnpdiscovery/upnpdiscovery.h index 080bddb4..f70d8129 100644 --- a/libguh/network/upnpdiscovery/upnpdiscovery.h +++ b/libguh/network/upnpdiscovery/upnpdiscovery.h @@ -24,19 +24,17 @@ #include #include #include -#include #include #include #include #include -#include -#include #include "upnpdiscoveryrequest.h" #include "upnpdevicedescriptor.h" #include "devicemanager.h" -// reference: http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf +// Discovering UPnP devices reference: http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf +// guh basic device reference: http://upnp.org/specs/basic/UPnP-basic-Basic-v1-Device.pdf class UpnpDiscoveryRequest; @@ -52,12 +50,15 @@ private: QHostAddress m_host; qint16 m_port; + QTimer *m_notificationTimer; + QNetworkAccessManager *m_networkAccessManager; QList m_discoverRequests; QHash m_informationRequestList; void requestDeviceInformation(const QNetworkRequest &networkRequest, const UpnpDeviceDescriptor &upnpDeviceDescriptor); + void respondToSearchRequest(); protected: @@ -69,6 +70,7 @@ private slots: void error(QAbstractSocket::SocketError error); void readData(); void replyFinished(QNetworkReply *reply); + void notificationTimeout(); void discoverTimeout(); }; diff --git a/server/server.pri b/server/server.pri index b2a6fd55..91e4374a 100644 --- a/server/server.pri +++ b/server/server.pri @@ -38,6 +38,7 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \ $$top_srcdir/server/rest/pluginsresource.cpp \ $$top_srcdir/server/rest/rulesresource.cpp \ + HEADERS += $$top_srcdir/server/guhcore.h \ $$top_srcdir/server/tcpserver.h \ $$top_srcdir/server/ruleengine.h \ diff --git a/server/server.pro b/server/server.pro index 97d6559b..506ba31b 100644 --- a/server/server.pro +++ b/server/server.pro @@ -8,7 +8,7 @@ INCLUDEPATH += ../libguh jsonrpc target.path = /usr/bin INSTALLS += target -QT += sql +QT += sql xml LIBS += -L$$top_builddir/libguh/ -lguh @@ -16,7 +16,7 @@ include(server.pri) include(qtservice/qtservice.pri) SOURCES += main.cpp \ - guhservice.cpp \ + guhservice.cpp boblight { xcompile { @@ -28,4 +28,4 @@ boblight { } HEADERS += \ - guhservice.h \ + guhservice.h diff --git a/server/webserver.cpp b/server/webserver.cpp index 2d0a62b5..426a00a4 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -1,4 +1,3 @@ - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2015 Simon Stuerz * @@ -79,6 +78,8 @@ #include "rest/restresource.h" #include +#include +#include #include #include #include @@ -118,6 +119,7 @@ WebServer::WebServer(const QSslConfiguration &sslConfiguration, QObject *parent) // check SSL if (m_useSsl && m_sslConfiguration.isNull()) m_useSsl = false; + } /*! Destructor of this \l{WebServer}. */ @@ -146,6 +148,25 @@ void WebServer::sendHttpReply(HttpReply *reply) socket->write(reply->data()); } +int WebServer::port() const +{ + return m_port; +} + +QList WebServer::serverAddressList() +{ + QList addresses; + foreach (const QNetworkInterface &interface, QNetworkInterface::allInterfaces()) { + // listen only on IPv4 + foreach (QNetworkAddressEntry entry, interface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { + addresses.append(entry.ip()); + } + } + } + return addresses; +} + bool WebServer::verifyFile(QSslSocket *socket, const QString &fileName) { QFileInfo file(fileName); @@ -195,6 +216,135 @@ QString WebServer::fileName(const QString &query) return m_webinterfaceDir.path() + fileName; } +QByteArray WebServer::createServerXmlDocument(QHostAddress address) +{ + GuhSettings settings(GuhSettings::SettingsRoleDevices); + settings.beginGroup("guhd"); + QByteArray uuid = settings.value("uuid", QVariant()).toByteArray(); + if (uuid.isEmpty()) { + uuid = QUuid::createUuid().toByteArray().replace("{", "").replace("}",""); + settings.setValue("uuid", uuid); + } + settings.endGroup(); + + + QByteArray data; + QXmlStreamWriter writer(&data); + writer.setAutoFormatting(true); + writer.writeStartDocument("1.0"); + writer.writeStartElement("root"); + writer.writeAttribute("xmlns", "urn:schemas-upnp-org:device-1-0"); + + writer.writeStartElement("specVersion"); + writer.writeTextElement("major", "1"); + writer.writeTextElement("minor", "1"); + writer.writeEndElement(); // specVersion + +// if (m_useSsl) { +// writer.writeTextElement("URLBase", "https://" + address.toString() + ":" + QString::number(m_port)); +// } else { +// writer.writeTextElement("URLBase", "http://" + address.toString() + ":" + QString::number(m_port)); +// } + writer.writeStartElement("device"); + writer.writeTextElement("deviceType", "urn:schemas-upnp-org:device:Basic:1"); + writer.writeTextElement("friendlyName", "guhd"); + writer.writeTextElement("manufacturer", "ARGE guh"); + writer.writeTextElement("manufacturerURL", "http://guh.guru"); + writer.writeTextElement("modelDescription", "Home automation server"); + writer.writeTextElement("modelName", "guhd"); + writer.writeTextElement("modelNumber", GUH_VERSION_STRING); + writer.writeTextElement("modelURL", "http://guh.io"); // (optional) + writer.writeTextElement("UDN", "uuid:" + uuid); + if (m_useSsl) { + writer.writeTextElement("presentationURL", "https://" + address.toString() + ":" + QString::number(m_port)); + } else { + writer.writeTextElement("presentationURL", "http://" + address.toString() + ":" + QString::number(m_port)); + } + + //writer.writeTextElement("presentationURL", "/"); + + writer.writeStartElement("iconList"); + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "8"); + writer.writeTextElement("height", "8"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-8x8.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "16"); + writer.writeTextElement("height", "16"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-16x16.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "22"); + writer.writeTextElement("height", "22"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-22x22.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "32"); + writer.writeTextElement("height", "32"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-32x32.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "48"); + writer.writeTextElement("height", "48"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-48x48.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "64"); + writer.writeTextElement("height", "64"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-64x64.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "128"); + writer.writeTextElement("height", "128"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-128x128.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "256"); + writer.writeTextElement("height", "256"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-256x256.png"); + writer.writeEndElement(); // icon + + writer.writeStartElement("icon"); + writer.writeTextElement("mimetype", "image/png"); + writer.writeTextElement("width", "512"); + writer.writeTextElement("height", "512"); + writer.writeTextElement("depth", "24"); + writer.writeTextElement("url", "/icons/guh-logo-512x512.png"); + writer.writeEndElement(); // icon + + writer.writeEndElement(); // iconList + + writer.writeEndElement(); // device + writer.writeEndElement(); // root + writer.writeEndDocument(); + return data; +} + HttpReply *WebServer::processIconRequest(const QString &fileName) { if (!fileName.endsWith(".png")) @@ -217,6 +367,17 @@ HttpReply *WebServer::processIconRequest(const QString &fileName) return RestResource::createErrorReply(HttpReply::NotFound); } +QHostAddress WebServer::getServerAddress(QHostAddress clientAddress) +{ + foreach (QHostAddress address, serverAddressList()) { + if (clientAddress.isInSubnet(QHostAddress::parseSubnet(address.toString() + "/24"))) { + qCDebug(dcWebServer) << "server for" << clientAddress.toString() << " ->" << address.toString(); + return address; + } + } + return QHostAddress(); +} + void WebServer::incomingConnection(qintptr socketDescriptor) { if (!m_enabled) @@ -363,6 +524,20 @@ void WebServer::readClient() return; } + // check server.xml call + if (request.url().path() == "/server.xml" && request.method() == HttpRequest::Get) { + qCDebug(dcWebServer) << "server XML request call"; + HttpReply *reply = RestResource::createSuccessReply(); + reply->setHeader(HttpReply::ContentTypeHeader, "text/xml"); + QHostAddress serverAddress = getServerAddress(socket->peerAddress()); + reply->setPayload(createServerXmlDocument(serverAddress)); + reply->setClientId(clientId); + sendHttpReply(reply); + reply->deleteLater(); + return; + } + + // request for a file... if (request.method() == HttpRequest::Get) { // check if the webinterface dir does exist, otherwise a filerequest is not relevant @@ -474,17 +649,20 @@ void WebServer::onError(QAbstractSocket::SocketError error) /*! Returns true if this \l{WebServer} started successfully. */ bool WebServer::startServer() { - if (!listen(QHostAddress::Any, m_port)) { + if (!listen(QHostAddress::AnyIPv4, m_port)) { qCWarning(dcConnection) << "Webserver could not listen on" << serverAddress().toString() << m_port; m_enabled = false; return false; } - if (m_useSsl) { - qCDebug(dcConnection) << "Started webserver on" << QString("https://%1:%2").arg(serverAddress().toString()).arg(m_port); - } else { - qCDebug(dcConnection) << "Started webserver on" << QString("http://%1:%2").arg(serverAddress().toString()).arg(m_port); + foreach (QHostAddress address, serverAddressList()) { + if (m_useSsl) { + qCDebug(dcConnection) << "Started webserver on" << QString("https://%1:%2").arg(address.toString()).arg(m_port); + } else { + qCDebug(dcConnection) << "Started webserver on" << QString("http://%1:%2").arg(address.toString()).arg(m_port); + } } + m_enabled = true; return true; } diff --git a/server/webserver.h b/server/webserver.h index 56ee4948..4f8d6a4f 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -75,6 +75,9 @@ public: ~WebServer(); void sendHttpReply(HttpReply *reply); + int port() const; + QList serverAddressList(); + private: QHash m_clientList; @@ -91,7 +94,9 @@ private: bool verifyFile(QSslSocket *socket, const QString &fileName); QString fileName(const QString &query); + QByteArray createServerXmlDocument(QHostAddress address); HttpReply *processIconRequest(const QString &fileName); + QHostAddress getServerAddress(QHostAddress clientAddress); protected: void incomingConnection(qintptr socketDescriptor) override;