This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
Michael Zanetti 192bdac209 Fix webserver tests
canonicalFilePath() behavior seems to have changed at some point
and not existing files outside the public dir will return a 404
rather than a 403. Ideally the logic would be fixed to first
check for the file being outside the public directory, directly
returning a 403, then checking if the file exists, return a 404
if not, and lastly check for permissions on the file and return
a 403 again of denied. However, that would result in a bigger
change.

Also, the tests were failing all along if syslog exists but is
not readable (code is ok, just bad test) but none of our
autotesters seemed to have such a setup till now.
2022-04-23 00:23:21 +02:00

830 lines
31 KiB
C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*!
\class nymeaserver::WebServer
\brief This class represents the web server for nymead.
\ingroup server
\inmodule core
The \l{WebServer} class provides a HTTP/1.1 web server. The web server
provides access to the nymea-webinterface and the path can be specified
in the \tt /etc/nymea/nymead.conf file and to the nymea \l{https://github.com/guh/nymea/wiki/REST-Api-documentation}{REST API}.
The default port for the web server is 3333, which is according to this
\l{https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers}{list}
officially free.
The URL for the insecure nymea-webinterface access:
\code http://localhost:3333\endcode
The URL for the secure HTTPS (TLS 1.2) nymea-webinterface access:
\code https://localhost:3333\endcode
The URL for the insecure REST API access to a \l{RestResource}:
\code http://localhost:3333/api/v1/{RestResource}\endcode
The URL for the secure HTTPS (TLS 1.2) REST API access to a \l{RestResource}:
\code https://localhost:3333/api/v1/{RestResource}\endcode
You can turn on the HTTPS server in the \tt WebServer section of the \tt /etc/nymea/nymead.conf file.
\note For \tt HTTPS you need to have a certificate and configure it in the \tt SSL-configuration
section of the \tt /etc/nymea/nymead.conf file.
\sa WebServerClient, WebSocketServer, TcpServer
*/
/*! \fn void nymeaserver::WebServer::httpRequestReady(const QUuid &clientId, const HttpRequest &httpRequest);
This signal is emitted when a \a httpRequest from a client with the given \a clientId is ready.
\sa RestServer, HttpRequest
*/
/*! \fn void nymeaserver::WebServer::clientConnected(const QUuid &clientId);
This signal is emitted when a new client with the given \a clientId has been connected.
*/
/*! \fn void nymeaserver::WebServer::clientDisconnected(const QUuid &clientId);
This signal is emitted when a client with the given \a clientId has been disconnected.
*/
/*! \fn void nymeaserver::WebServer::incomingConnection(qintptr socketDescriptor);
Overwritten virtual method from \l{http://doc.qt.io/qt-5/qtcpserver.html#incomingConnection}{QTcpServer::incomingConnection( \a socketDescriptor)}.
*/
#include "webserver.h"
#include "loggingcategories.h"
#include "nymeasettings.h"
#include "nymeacore.h"
#include "httpreply.h"
#include "httprequest.h"
#include "debugserverhandler.h"
#include "version.h"
#include <QJsonDocument>
#include <QNetworkInterface>
#include <QXmlStreamWriter>
#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <QSslSocket>
#include <QUrlQuery>
#include <QUuid>
#include <QUrl>
#include <QFile>
namespace nymeaserver {
/*! Constructs a \l{WebServer} with the given \a configuration, \a sslConfiguration and \a parent.
*
* \sa ServerManager, WebServerConfiguration
*/
WebServer::WebServer(const WebServerConfiguration &configuration, const QSslConfiguration &sslConfiguration, QObject *parent) :
QTcpServer(parent),
m_configuration(configuration),
m_sslConfiguration(sslConfiguration)
{
if (QCoreApplication::instance()->organizationName() == "nymea-test") {
m_configuration.publicFolder = QCoreApplication::applicationDirPath();
}
qCDebug(dcWebServer()) << "Starting WebServer. Interface:" << m_configuration.address << "Port:" << m_configuration.port << "SSL:" << m_configuration.sslEnabled << "AUTH:" << m_configuration.authenticationEnabled << "Public folder:" << QDir(m_configuration.publicFolder).canonicalPath();
}
/*! Destructor of this \l{WebServer}. */
WebServer::~WebServer()
{
qCDebug(dcWebServer()) << "Shutting down \"Webserver\"" << serverUrl().toString();
this->close();
}
/*! Returns the server URL of this WebServer. */
QUrl WebServer::serverUrl() const
{
return QUrl(QString("%1://%2:%3").arg((m_configuration.sslEnabled ? "https" : "http")).arg(m_configuration.address).arg(m_configuration.port));
}
/*! Send the given \a reply map to the corresponding client.
*
* \sa HttpReply
*/
void WebServer::sendHttpReply(HttpReply *reply)
{
// get the right socket
QSslSocket *socket = nullptr;
socket = m_clientList.value(reply->clientId());
if (!socket) {
qCWarning(dcWebServer()) << "Invalid socket pointer! This should never happen!!! Missing clientId in reply?";
return;
}
// send raw data
reply->packReply();
qCDebug(dcWebServerTraffic()) << "Send reply to" << socket->peerAddress().toString() << reply;
qCDebug(dcWebServer()) << "Respond" << socket->peerAddress().toString() << reply->httpStatusCode() << reply->httpReasonPhrase();
socket->write(reply->data());
}
bool WebServer::verifyFile(QSslSocket *socket, const QString &fileName)
{
QFileInfo file(fileName);
// make sure the file exists
if (!file.exists()) {
qCDebug(dcWebServer()) << "requested file" << file.filePath() << "does not exist.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound);
reply->setClientId(m_clientList.key(socket));
sendHttpReply(reply);
reply->deleteLater();
return false;
}
// make sure the file is in the public directory
if (!file.canonicalFilePath().startsWith(QDir(m_configuration.publicFolder).canonicalPath())) {
qCDebug(dcWebServer()) << "Requested file" << file.canonicalFilePath() << "is outside the public folder.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::Forbidden);
reply->setClientId(m_clientList.key(socket));
sendHttpReply(reply);
reply->deleteLater();
return false;
}
// make sure we can read the file
if (!file.isReadable()) {
qCDebug(dcWebServer()) << "Requested file" << file.fileName() << "is not readable.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::Forbidden);
reply->setClientId(m_clientList.key(socket));
reply->setPayload("403 Forbidden. File not readable");
sendHttpReply(reply);
reply->deleteLater();
return false;
}
return true;
}
QString WebServer::fileName(const QString &query)
{
QString fileName;
if (query.isEmpty() || query == "/") {
fileName = "/index.html";
} else {
fileName = query;
}
return m_configuration.publicFolder + "/" + fileName;
}
HttpReply *WebServer::processIconRequest(const QString &fileName)
{
if (!fileName.endsWith(".png"))
return HttpReply::createErrorReply(HttpReply::NotFound);
QByteArray imageData;
QImage image(":" + fileName);
QBuffer buffer(&imageData);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "png");
if (!imageData.isEmpty()) {
HttpReply *reply = HttpReply::createSuccessReply();
reply->setHeader(HttpReply::ContentTypeHeader, "image/png");
reply->setPayload(imageData);
return reply;
}
return HttpReply::createErrorReply(HttpReply::NotFound);
}
void WebServer::incomingConnection(qintptr socketDescriptor)
{
if (!m_enabled)
return;
QSslSocket *socket = new QSslSocket();
if (!socket->setSocketDescriptor(socketDescriptor)) {
qCWarning(dcWebServer()) << "Could not set socket descriptor. Rejecting connection.";
socket->close();
delete socket;
return;
}
// check webserver client
bool existing = false;
foreach (WebServerClient *client, m_webServerClients) {
if (client->address() == socket->peerAddress()) {
if (client->connections().count() >= 50) {
qCWarning(dcWebServer()).noquote() << QString("Maximum connections for this client reached: rejecting connection from client %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort());
socket->close();
delete socket;
return;
}
client->addConnection(socket);
existing = true;
break;
}
}
if (!existing) {
WebServerClient *webServerClient = new WebServerClient(socket->peerAddress());
webServerClient->addConnection(socket);
m_webServerClients.append(webServerClient);
}
// append the new client to the client list
QUuid clientId = QUuid::createUuid();
m_clientList.insert(clientId, socket);
qCDebug(dcWebServer()).noquote() << QString("Webserver client %1:%2 connected").arg(socket->peerAddress().toString()).arg(socket->peerPort());
if (m_configuration.sslEnabled) {
// configure client connection
socket->setSslConfiguration(m_sslConfiguration);
connect(socket, SIGNAL(encrypted()), this, SLOT(onEncrypted()));
socket->startServerEncryption();
// wait for encrypted connection before continue with this client
return;
}
connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(socket, SIGNAL(disconnected()), this, SLOT(onDisconnected()));
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError)));
emit clientConnected(clientId);
}
void WebServer::readClient()
{
if (!m_enabled)
return;
QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
QUuid clientId = m_clientList.key(socket);
// Check client
if (clientId.isNull()) {
qCWarning(dcWebServer()) << "Client not recognized";
socket->close();
return;
}
// Read HTTP request
QByteArray data = socket->readAll();
HttpRequest request;
if (m_incompleteRequests.contains(socket)) {
request = m_incompleteRequests.take(socket);
request.appendData(data);
} else {
request = HttpRequest(data);
}
// Check if the request is complete
if (!request.isComplete()) {
m_incompleteRequests.insert(socket, request);
return;
}
qCDebug(dcWebServerTraffic()) << "Received request from" << clientId.toString() << socket->peerAddress().toString() << request;
// Check if the request is valid
if (!request.isValid()) {
qCDebug(dcWebServer()) << "Got invalid request:" << request.url().path();
HttpReply *reply = HttpReply::createErrorReply(HttpReply::BadRequest);
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
return;
}
// Check HTTP version
if (request.httpVersion() != "HTTP/1.1" && request.httpVersion() != "HTTP/1.0") {
qCDebug(dcWebServer()) << "HTTP version is not supported." << request.httpVersion();
HttpReply *reply = HttpReply::createErrorReply(HttpReply::HttpVersionNotSupported);
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
return;
}
qCDebug(dcWebServer()).noquote() << QString("Got valid request from %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()) << request.methodString() << request.url().path() << request.urlQuery().toString();
// Reset timout
foreach (WebServerClient *webserverClient, m_webServerClients) {
if (webserverClient->address() == socket->peerAddress()) {
webserverClient->resetTimout(socket);
break;
}
}
// Verify method
if (request.method() == HttpRequest::Unhandled) {
HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed);
reply->setClientId(clientId);
reply->setHeader(HttpReply::AllowHeader, "GET, PUT, POST, DELETE, OPTIONS");
sendHttpReply(reply);
reply->deleteLater();
return;
}
// Check icon call
if (request.url().path().startsWith("/icons/") && request.method() == HttpRequest::Get) {
HttpReply *reply = processIconRequest(request.url().path());
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
return;
}
// Check if this is a debug call
if (request.url().path().startsWith("/debug")) {
// Check if debug server is enabled
if (NymeaCore::instance()->configuration()->debugServerEnabled()) {
// Verify methods
if (request.method() != HttpRequest::Get && request.method() != HttpRequest::Options) {
HttpReply *reply = HttpReply::createErrorReply(HttpReply::MethodNotAllowed);
reply->setClientId(clientId);
reply->setHeader(HttpReply::AllowHeader, "GET, OPTIONS");
sendHttpReply(reply);
reply->deleteLater();
return;
}
qCDebug(dcDebugServer()) << "Request:" << request.url().toString();
HttpReply *reply = NymeaCore::instance()->debugServerHandler()->processDebugRequest(request.url().path(), request.urlQuery());
reply->setClientId(clientId);
// Handle async replies
if (reply->type() == HttpReply::TypeAsync) {
connect(reply, &HttpReply::finished, this, &WebServer::onAsyncReplyFinished);
reply->startWait();
} else {
sendHttpReply(reply);
reply->deleteLater();
}
return;
} else {
qCWarning(dcWebServer()) << "The debug server handler is disabled. You can enable it by adding \'debugServerEnabled=true\' in the \'nymead\' section of the nymead.conf file.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound);
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
return;
}
}
// Check server.xml call
if (request.url().path() == "/server.xml" && request.method() == HttpRequest::Get) {
qCDebug(dcWebServer()) << "Server XML request call";
HttpReply *reply = HttpReply::createSuccessReply();
reply->setHeader(HttpReply::ContentTypeHeader, "text/xml");
reply->setPayload(createServerXmlDocument(socket->localAddress()));
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
// FIXME: return a default webpage containing server information
if (!QDir(m_configuration.publicFolder).exists()) {
qDebug(dcWebServer()) << "Webinterface folder" << m_configuration.publicFolder << "does not exist.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound);
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
return;
}
QString path = fileName(request.url().path());
if (!verifyFile(socket, path))
return;
QFile file(path);
if (file.open(QFile::ReadOnly)) {
qCDebug(dcWebServer()) << "Load file" << file.fileName();
HttpReply *reply = HttpReply::createSuccessReply();
// Check content type
if (file.fileName().endsWith(".html")) {
reply->setHeader(HttpReply::ContentTypeHeader, "text/html; charset=\"utf-8\";");
} else if (file.fileName().endsWith(".css")) {
reply->setHeader(HttpReply::ContentTypeHeader, "text/css; charset=\"utf-8\";");
} else if (file.fileName().endsWith(".pdf")) {
reply->setHeader(HttpReply::ContentTypeHeader, "application/pdf");
} else if (file.fileName().endsWith(".js")) {
reply->setHeader(HttpReply::ContentTypeHeader, "text/javascript; charset=\"utf-8\";");
} else if (file.fileName().endsWith(".ttf")) {
reply->setHeader(HttpReply::ContentTypeHeader, "application/x-font-ttf");
} else if (file.fileName().endsWith(".eot")) {
reply->setHeader(HttpReply::ContentTypeHeader, "application/vnd.ms-fontobject");
} else if (file.fileName().endsWith(".woff")) {
reply->setHeader(HttpReply::ContentTypeHeader, "application/x-font-woff");
} else if (file.fileName().endsWith(".jpg") || file.fileName().endsWith(".jpeg")) {
reply->setHeader(HttpReply::ContentTypeHeader, "image/jpeg");
} else if (file.fileName().endsWith(".png") || file.fileName().endsWith(".PNG")) {
reply->setHeader(HttpReply::ContentTypeHeader, "image/png");
} else if (file.fileName().endsWith(".ico")) {
reply->setHeader(HttpReply::ContentTypeHeader, "image/x-icon");
} else if (file.fileName().endsWith(".svg")) {
reply->setHeader(HttpReply::ContentTypeHeader, "image/svg+xml; charset=\"utf-8\";");
}
reply->setPayload(file.readAll());
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
return;
}
}
// Reject everything else...
qCDebug(dcWebServer()) << "Unknown message received.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotImplemented);
reply->setClientId(clientId);
sendHttpReply(reply);
reply->deleteLater();
}
void WebServer::onDisconnected()
{
QSslSocket* socket = static_cast<QSslSocket *>(sender());
// Remove connection from server client
foreach (WebServerClient *client, m_webServerClients) {
if (client->address() == socket->peerAddress()) {
client->removeConnection(socket);
if (client->connections().isEmpty()) {
m_webServerClients.removeAll(client);
client->deleteLater();
}
break;
}
}
qCDebug(dcWebServer()).noquote() << QString("Webserver client disonnected %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort());
// clean up
QUuid clientId = m_clientList.key(socket);
m_clientList.remove(clientId);
m_incompleteRequests.remove(socket);
emit clientDisconnected(clientId);
socket->deleteLater();
}
void WebServer::onEncrypted()
{
QSslSocket* socket = static_cast<QSslSocket *>(sender());
qCDebug(dcWebServer()).noquote() << QString("Encrypted connection %1:%2 successfully established.").arg(socket->peerAddress().toString()).arg(socket->peerPort());
connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(socket, SIGNAL(disconnected()), this, SLOT(onDisconnected()));
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError)));
emit clientConnected(m_clientList.key(socket));
}
void WebServer::onError(QAbstractSocket::SocketError error)
{
QSslSocket* socket = static_cast<QSslSocket *>(sender());
switch (error) {
case QAbstractSocket::RemoteHostClosedError:
qCDebug(dcWebServer()).noquote() << QString("Client socket error %1:%2 ->").arg(socket->peerAddress().toString()).arg(socket->peerPort()) << socket->errorString();
break;
default:
qCWarning(dcWebServer()).noquote() << QString("Client socket error %1:%2 ->").arg(socket->peerAddress().toString()).arg(socket->peerPort()) << socket->errorString();
break;
}
}
void WebServer::onAsyncReplyFinished()
{
HttpReply *reply = qobject_cast<HttpReply*>(sender());
qCDebug(dcWebServer()) << "Async reply finished";
// check if the reply timeouted
if (reply->timedOut()) {
reply->clear();
reply->setHttpStatusCode(HttpReply::GatewayTimeout);
}
sendHttpReply(reply);
reply->deleteLater();
}
/*! Set the configuration of this \l{WebServer} to the given \a config.
*
* \sa WebServerConfiguration
*/
void WebServer::setConfiguration(const WebServerConfiguration &config)
{
m_configuration = config;
}
/*! Sets the server name to the given \a serverName. */
void WebServer::setServerName(const QString &serverName)
{
m_serverName = serverName;
}
/*! Returns true if this \l{WebServer} started successfully. */
bool WebServer::startServer()
{
if (!listen(QHostAddress(m_configuration.address), static_cast<quint16>(m_configuration.port))) {
qCWarning(dcWebServer()) << "Webserver could not listen on" << serverUrl().toString() << errorString();
m_enabled = false;
return false;
}
qCDebug(dcWebServer()) << "Started web server on" << serverUrl().toString();
m_enabled = true;
return true;
}
/*! Returns true if this \l{WebServer} stopped successfully. */
bool WebServer::stopServer()
{
foreach (QSslSocket *client, m_clientList.values())
client->close();
close();
m_enabled = false;
qCDebug(dcWebServer()) << "Webserver closed.";
return true;
}
WebServerConfiguration WebServer::configuration() const
{
return m_configuration;
}
QByteArray WebServer::createServerXmlDocument(QHostAddress address)
{
QByteArray uuid = NymeaCore::instance()->configuration()->serverUuid().toString().remove(QRegExp("[{}]")).toUtf8();
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
QString presentationUrl = QString("%1://%2:%3")
.arg(m_configuration.sslEnabled ? "https" : "http")
.arg(address.toString())
.arg(m_configuration.port);
writer.writeStartElement("device");
writer.writeTextElement("presentationURL", presentationUrl);
writer.writeTextElement("deviceType", "urn:schemas-upnp-org:device:Basic:1");
writer.writeTextElement("friendlyName", NymeaCore::instance()->configuration()->serverName());
writer.writeTextElement("manufacturer", "nymea GmbH");
writer.writeTextElement("manufacturerURL", "http://nymea.io");
writer.writeTextElement("modelDescription", "IoT server");
writer.writeTextElement("modelName", "nymead");
writer.writeTextElement("modelNumber", NYMEA_VERSION_STRING);
writer.writeTextElement("modelURL", "http://nymea.io"); // (optional)
writer.writeTextElement("UDN", "uuid:" + uuid);
writer.writeStartElement("iconList");
writer.writeStartElement("icon");
writer.writeTextElement("mimetype", "image/png");
writer.writeTextElement("width", "8");
writer.writeTextElement("height", "8");
writer.writeTextElement("depth", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-logo-64x64.png");
writer.writeEndElement(); // icon
writer.writeStartElement("icon");
writer.writeTextElement("mimetype", "image/png");
writer.writeTextElement("width", "120");
writer.writeTextElement("height", "120");
writer.writeTextElement("depth", "8");
writer.writeTextElement("url", "/icons/nymea-logo-120x120.png");
writer.writeEndElement(); // icon
writer.writeStartElement("icon");
writer.writeTextElement("mimetype", "image/png");
writer.writeTextElement("width", "128");
writer.writeTextElement("height", "128");
writer.writeTextElement("depth", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-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", "8");
writer.writeTextElement("url", "/icons/nymea-logo-512x512.png");
writer.writeEndElement(); // icon
writer.writeEndElement(); // iconList
writer.writeStartElement("serviceList");
int counter = 1;
int sslCounter = 1;
foreach (const ServerConfiguration &config, NymeaCore::instance()->configuration()->webSocketServerConfigurations()) {
if (QHostAddress(config.address) == QHostAddress("0.0.0.0") || QHostAddress(config.address) == address) {
writer.writeStartElement("service");
writer.writeTextElement("serviceType", QString("urn:nymea.io:service:%1:%2").arg(config.sslEnabled ? "wss" : "ws").arg(config.sslEnabled ? sslCounter : counter));
writer.writeTextElement("serviceId", QString("urn:nymea.io:serviceId:%1:%2").arg(config.sslEnabled ? "wss" : "ws").arg(config.sslEnabled ? sslCounter++ : counter++));
QString url = QString("%1%2:%3").arg(config.sslEnabled ? "wss://" : "ws://").arg(address.toString()).arg(config.port);
writer.writeTextElement("SCPDURL", url);
writer.writeEndElement(); // service
}
}
counter = 1;
sslCounter = 1;
foreach (const ServerConfiguration &config, NymeaCore::instance()->configuration()->tcpServerConfigurations()) {
if (QHostAddress(config.address) == QHostAddress("0.0.0.0") || QHostAddress(config.address) == address) {
writer.writeStartElement("service");
writer.writeTextElement("serviceType", QString("urn:nymea.io:service:%1:%2").arg(config.sslEnabled ? "nymeas" : "nymea").arg(config.sslEnabled ? sslCounter : counter));
writer.writeTextElement("serviceId", QString("urn:nymea.io:serviceId:%1:%2").arg(config.sslEnabled ? "nymeas" : "nymea").arg(config.sslEnabled ? sslCounter++ : counter++));
QString url = QString("%1%2:%3").arg(config.sslEnabled ? "nymeas://" : "nymea://").arg(address.toString()).arg(config.port);
writer.writeTextElement("SCPDURL", url);
writer.writeEndElement(); // service
}
}
writer.writeEndElement(); // serviceList
writer.writeEndElement(); // device
writer.writeEndElement(); // root
writer.writeEndDocument();
return data;
}
/*!
\class nymeaserver::WebServerClient
\brief This class represents a client the web server for nymead.
\ingroup server
\inmodule core
The \l{WebServerClient} represents a client for the nymea \l{WebServer}. Each client can
have up to 50 connections and each connection will timeout after 12 seconds if the
connection will not be used.
If all connections of a \l{WebServerClient} are closed, the client will be removed from
system.
\sa WebServer
*/
/*! Constructs a \l{WebServerClient} with the given \a address and \a parent. */
WebServerClient::WebServerClient(const QHostAddress &address, QObject *parent):
QObject(parent),
m_address(address)
{
}
/*! Returns the address of this \l{WebServerClient}. */
QHostAddress WebServerClient::address() const
{
return m_address;
}
/*! Returns the list of connections (sockets) of this \l{WebServerClient}. */
QList<QSslSocket *> WebServerClient::connections()
{
return m_connections;
}
/*! Adds a new connection (\a socket) to this \l{WebServerClient}. A \l{WebServerClient}
* can have up to 50 connecections. The connection will timout and closed if the client
* does not use the connection for 65 seconds.
*/
void WebServerClient::addConnection(QSslSocket *socket)
{
QTimer *timer = new QTimer(this);
timer->setSingleShot(true);
timer->setInterval(65000);
connect(timer, &QTimer::timeout, this, &WebServerClient::onTimout);
m_runningConnections.insert(timer, socket);
m_connections.append(socket);
timer->start();
}
/*! Removes a connection the given \a socket from the connection list of this \l{WebServerClient}. */
void WebServerClient::removeConnection(QSslSocket *socket)
{
QTimer *timer = m_runningConnections.key(socket);
m_runningConnections.remove(timer);
m_connections.removeAll(socket);
delete timer;
}
/*! Resets the connection timeout for the given \a socket. If the socket will not be used for 12 seconds the
* connection will be closed.
*/
void WebServerClient::resetTimout(QSslSocket *socket)
{
QTimer *timer = nullptr;
timer = m_runningConnections.key(socket);
if (timer)
timer->start();
}
void WebServerClient::onTimout()
{
QTimer *timer = static_cast<QTimer *>(sender());
QSslSocket *socket = m_runningConnections.value(timer);
qCDebug(dcWebServer()).noquote() << QString("Client connection timout %1:%2 -> closing connection").arg(socket->peerAddress().toString()).arg(socket->peerPort());
removeConnection(socket);
socket->close();
}
}