diff --git a/data/config/guhd.conf b/data/config/guhd.conf index 4c1347f3..c0a6302a 100644 --- a/data/config/guhd.conf +++ b/data/config/guhd.conf @@ -1,13 +1,14 @@ [JSONRPC] port=1234 -interfaces="lo","all" +interfaces="lo", "all" ip="IPv4", "IPv6" [Webserver] port=3000 -publicFolder=/usr/share/guh-webinterface/ https=false - +certificate=/etc/ssl/certs/guhd-certificate.crt +certificate-key=/etc/ssl/private/guhd-certificate.key +publicFolder=/usr/share/guh-webinterface/public [GPIO] rf433rx=27 diff --git a/debian/changelog b/debian/changelog index 9f1fc942..e36dda51 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +guh (0.5.0) vivid; urgency=medium + + * Add webserver and REST API + + -- Simon Stürz Sat, 01 Aug 2015 14:52:12 +0200 + guh (0.4.0) vivid; urgency=medium * add EditDevice and notifications diff --git a/server/httprequest.cpp b/server/httprequest.cpp index 5703818b..dada315b 100644 --- a/server/httprequest.cpp +++ b/server/httprequest.cpp @@ -204,7 +204,7 @@ HttpRequest::RequestMethod HttpRequest::getRequestMethodType(const QString &meth QDebug operator<<(QDebug debug, const HttpRequest &httpRequest) { - debug << "\n===================================" << "\n"; + debug << "===================================" << "\n"; debug << " HTTP version: " << httpRequest.httpVersion() << "\n"; debug << " method: " << httpRequest.methodString() << "\n"; debug << " URL path: " << httpRequest.url().path() << "\n"; diff --git a/server/tcpserver.cpp b/server/tcpserver.cpp index e654aff1..c30df41f 100644 --- a/server/tcpserver.cpp +++ b/server/tcpserver.cpp @@ -209,7 +209,7 @@ void TcpServer::onClientDisconnected() m_clientList.take(clientId)->deleteLater(); } -void TcpServer::onError(const QAbstractSocket::SocketError &error) +void TcpServer::onError(QAbstractSocket::SocketError error) { QTcpServer *server = qobject_cast(sender()); QUuid uuid = m_serverList.key(server); diff --git a/server/tcpserver.h b/server/tcpserver.h index 554bba46..d4501100 100644 --- a/server/tcpserver.h +++ b/server/tcpserver.h @@ -63,7 +63,7 @@ private slots: void onClientConnected(); void onClientDisconnected(); void readPackage(); - void onError(const QAbstractSocket::SocketError &error); + void onError(QAbstractSocket::SocketError error); void onTimeout(); public slots: diff --git a/server/transportinterface.h b/server/transportinterface.h index 62689675..bd52ce96 100644 --- a/server/transportinterface.h +++ b/server/transportinterface.h @@ -21,7 +21,7 @@ #ifndef TRANSPORTINTERFACE_H #define TRANSPORTINTERFACE_H -#include +#include #include #include #include diff --git a/server/webserver.cpp b/server/webserver.cpp index 89f707ef..54257f87 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -35,36 +36,49 @@ namespace guhserver { WebServer::WebServer(QObject *parent) : - TransportInterface(parent), - m_enabled(false) + QTcpServer(parent), + m_enabled(false), + m_useSsl(false) { // load webserver settings GuhSettings settings(GuhSettings::SettingsRoleGlobal); - qCDebug(dcTcpServer) << "Loading webserver settings from:" << settings.fileName(); + qCDebug(dcWebServer) << "Loading webserver settings from:" << settings.fileName(); settings.beginGroup("Webserver"); m_port = settings.value("port", 3000).toInt(); + m_useSsl = settings.value("https", false).toBool(); m_webinterfaceDir = QDir(settings.value("publicFolder", "/usr/share/guh-webinterface/public/").toString()); + QString certificateFileName = settings.value("certificate", QVariant("/etc/ssl/certs/guhd-certificate.crt")).toString(); + QString keyFileName = settings.value("certificate-key", QVariant("/etc/ssl/private/guhd-certificate.key")).toString(); settings.endGroup(); - qCDebug(dcTcpServer) << "Publish webinterface from" << m_webinterfaceDir.path(); - + // check public directory + qCDebug(dcWebServer) << "Publish webinterface folder" << m_webinterfaceDir.path(); if (!m_webinterfaceDir.exists()) - qCWarning(dcWebServer) << "Web interface path" << m_webinterfaceDir.path() << "does not exist."; + qCWarning(dcWebServer) << "Web interface public folder" << m_webinterfaceDir.path() << "does not exist."; - // create webserver - m_server = new QTcpServer(this); - connect(m_server, &QTcpServer::newConnection, this, &WebServer::onNewConnection); + // check SSL + if (m_useSsl && !QSslSocket::supportsSsl()) { + qCWarning(dcWebServer) << "SSL is not supported/installed on this platform."; + m_useSsl = false; + } + + if (m_useSsl && !loadCertificate(keyFileName, certificateFileName)) { + qCWarning(dcWebServer) << "SSL encryption disabled"; + m_useSsl = false; + return; + } + qCDebug(dcWebServer) << "Using SSL lib version:" << QSslSocket::sslLibraryVersionString(); } WebServer::~WebServer() { - m_server->close(); + this->close(); } void WebServer::sendData(const QUuid &clientId, const QVariantMap &data) { - QTcpSocket *socket = m_clientList.value(clientId); + QSslSocket *socket = m_clientList.value(clientId); HttpReply reply(HttpReply::Ok); reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); reply.setPayload(QJsonDocument::fromVariant(data).toJson()); @@ -75,7 +89,7 @@ void WebServer::sendData(const QUuid &clientId, const QVariantMap &data) void WebServer::sendData(const QList &clients, const QVariantMap &data) { foreach (const QUuid &client, clients) { - QTcpSocket *socket = m_clientList.value(client); + QSslSocket *socket = m_clientList.value(client); HttpReply reply(HttpReply::Ok); reply.setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); reply.setPayload(QJsonDocument::fromVariant(data).toJson()); @@ -86,7 +100,7 @@ void WebServer::sendData(const QList &clients, const QVariantMap &data) void WebServer::sendHttpReply(HttpReply *reply) { - QTcpSocket *socket = 0; + QSslSocket *socket = 0; socket = m_clientList.value(reply->clientId()); if (!socket) { @@ -96,7 +110,7 @@ void WebServer::sendHttpReply(HttpReply *reply) writeData(socket, reply->data()); } -bool WebServer::verifyFile(QTcpSocket *socket, const QString &fileName) +bool WebServer::verifyFile(QSslSocket *socket, const QString &fileName) { QFileInfo file(fileName); @@ -146,40 +160,83 @@ QString WebServer::fileName(const QString &query) return m_webinterfaceDir.path() + fileName; } -void WebServer::writeData(QTcpSocket *socket, const QByteArray &data) +bool WebServer::loadCertificate(const QString &keyFileName, const QString &certificateFileName) { - QTextStream os(socket); - os.setAutoDetectUnicode(true); - os << data; + QByteArray certificateData; + QByteArray certificateKeyData; + + QFile certificateKeyFile(keyFileName); + if (!certificateKeyFile.open(QIODevice::ReadOnly)) { + qCWarning(dcWebServer) << "Could not open" << certificateKeyFile.fileName() << ":" << certificateKeyFile.errorString(); + return false; + } + certificateKeyData = certificateKeyFile.readAll(); + certificateKeyFile.close(); + qCDebug(dcWebServer) << "Loaded successfully private certificate key."; + + QFile certificateFile(certificateFileName); + if (!certificateFile.open(QIODevice::ReadOnly)) { + qCWarning(dcWebServer) << "Could not open" << certificateFile.fileName() << ":" << certificateFile.errorString(); + return false; + } + certificateData = certificateFile.readAll(); + certificateFile.close(); + qCDebug(dcWebServer) << "Loaded successfully certificate file."; + + m_certificate = QSslCertificate(certificateData); + m_certificateKey = QSslKey(certificateKeyData, QSsl::Rsa); + return true; +} + +void WebServer::writeData(QSslSocket *socket, const QByteArray &data) +{ + socket->write(data); socket->close(); } -void WebServer::onNewConnection() +void WebServer::incomingConnection(qintptr socketDescriptor) { if (!m_enabled) return; - QTcpSocket* socket = m_server->nextPendingConnection(); + QSslSocket *socket = new QSslSocket(); + if (!socket->setSocketDescriptor(socketDescriptor)) { + qCWarning(dcConnection) << "Could not set socket descriptor. Rejecting connection."; + delete socket; + return; + } // append the new client to the client list QUuid clientId = QUuid::createUuid(); m_clientList.insert(clientId, socket); - connect(socket, &QTcpSocket::readyRead, this, &WebServer::readClient); - connect(socket, &QTcpSocket::disconnected, this, &WebServer::onDisconnected); - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); + qCDebug(dcConnection) << QString("Webserver client %1:%2 connected").arg(socket->peerAddress().toString()).arg(socket->peerPort()); - qCDebug(dcConnection) << "Webserver client connected" << socket->peerName() << socket->peerAddress().toString() << socket->peerPort(); + if (m_useSsl) { + // configure client connection + socket->setProtocol(QSsl::TlsV1_2); + socket->setPrivateKey(m_certificateKey); + socket->setLocalCertificate(m_certificate); + connect(socket, SIGNAL(encrypted()), this, SLOT(onEncrypted())); + socket->startServerEncryption(); + // wait for encrypted connection before continue + 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; - QTcpSocket *socket = qobject_cast(sender()); + QSslSocket *socket = qobject_cast(sender()); QUuid clientId = m_clientList.key(socket); // check client @@ -243,7 +300,7 @@ void WebServer::readClient() } // request for a file... - if (request.method() == HttpRequest::Get) { + if (request.method() == HttpRequest::Get && m_webinterfaceDir.exists()) { QString path = fileName(request.url().path()); if (!verifyFile(socket, path)) return; @@ -264,13 +321,13 @@ void WebServer::readClient() // reject everything else... qCWarning(dcWebServer) << "Unknown message received. Respond client with 501: Not Implemented."; HttpReply reply(HttpReply::NotImplemented); - reply.setPayload("501 Not implemented."); + reply.setPayload("404 Not found."); writeData(socket, reply.data()); } void WebServer::onDisconnected() { - QTcpSocket* socket = qobject_cast(sender()); + QSslSocket* socket = static_cast(sender()); qCDebug(dcConnection) << "Webserver client disonnected."; // clean up @@ -282,28 +339,42 @@ void WebServer::onDisconnected() emit clientDisconnected(clientId); } +void WebServer::onEncrypted() +{ + QSslSocket* socket = static_cast(sender()); + qCDebug(dcConnection) << 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) { - QTcpSocket* socket = qobject_cast(sender()); + QSslSocket* socket = static_cast(sender()); qCWarning(dcConnection) << "Client socket error" << socket->peerAddress() << error << socket->errorString(); } bool WebServer::startServer() { - if (!m_server->listen(QHostAddress::Any, m_port)) { - qCWarning(dcConnection) << "Webserver could not listen on" << m_server->serverAddress().toString() << m_port; + if (!listen(QHostAddress::Any, m_port)) { + qCWarning(dcConnection) << "Webserver could not listen on" << serverAddress().toString() << m_port; m_enabled = false; return false; } - qCDebug(dcConnection) << "Started webserver on" << QString("http://%1:%2").arg(m_server->serverAddress().toString()).arg(m_port); + 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); + } m_enabled = true; - return true; } bool WebServer::stopServer() { - m_server->close(); + close(); m_enabled = false; qCDebug(dcConnection) << "Webserver closed."; return true; diff --git a/server/webserver.h b/server/webserver.h index a0137a43..996b6d29 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -22,14 +22,14 @@ #define WEBSERVER_H #include +#include +#include #include #include -#include -#include +#include +#include +#include -#include "transportinterface.h" - -class QTcpServer; class HttpRequest; class HttpReply; @@ -38,47 +38,53 @@ class HttpReply; namespace guhserver { -class WebServer : public TransportInterface +class WebServer : public QTcpServer { Q_OBJECT public: - - explicit WebServer(QObject *parent = 0); ~WebServer(); - void sendData(const QUuid &clientId, const QVariantMap &data) override; - void sendData(const QList &clients, const QVariantMap &data) override; + void sendData(const QUuid &clientId, const QVariantMap &data); + void sendData(const QList &clients, const QVariantMap &data); void sendHttpReply(HttpReply *reply); private: - QTcpServer *m_server; - - QHash m_clientList; - QHash m_incompleteRequests; + QHash m_clientList; + QHash m_incompleteRequests; bool m_enabled; + bool m_useSsl; qint16 m_port; QDir m_webinterfaceDir; + QSslCertificate m_certificate; + QSslKey m_certificateKey; - bool verifyFile(QTcpSocket *socket, const QString &fileName); - + bool verifyFile(QSslSocket *socket, const QString &fileName); QString fileName(const QString &query); - void writeData(QTcpSocket *socket, const QByteArray &data); + bool loadCertificate(const QString &keyFileName, const QString &certificateFileName); + + void writeData(QSslSocket *socket, const QByteArray &data); + +protected: + void incomingConnection(qintptr socketDescriptor) override; signals: void httpRequestReady(const QUuid &clientId, const HttpRequest &httpRequest); + void clientConnected(const QUuid &clientId); + void clientDisconnected(const QUuid &clientId); + void dataAvailable(const QUuid &clientId, const QString &targetNamespace, const QString &method, const QVariantMap &message); private slots: - void onNewConnection(); void readClient(); void onDisconnected(); + void onEncrypted(); void onError(QAbstractSocket::SocketError error); public slots: - bool startServer() override; - bool stopServer() override; + bool startServer(); + bool stopServer(); }; diff --git a/server/websocketserver.cpp b/server/websocketserver.cpp index 4af45f87..d4f13cb8 100644 --- a/server/websocketserver.cpp +++ b/server/websocketserver.cpp @@ -25,7 +25,32 @@ namespace guhserver { WebSocketServer::WebSocketServer(QObject *parent) : TransportInterface(parent) { +} +WebSocketServer::~WebSocketServer() +{ +} + +void WebSocketServer::sendData(const QUuid &clientId, const QVariantMap &data) +{ + Q_UNUSED(clientId) + Q_UNUSED(data) +} + +void WebSocketServer::sendData(const QList &clients, const QVariantMap &data) +{ + Q_UNUSED(clients) + Q_UNUSED(data) +} + +bool WebSocketServer::startServer() +{ + return false; +} + +bool WebSocketServer::stopServer() +{ + return false; } } diff --git a/server/websocketserver.h b/server/websocketserver.h index 829ecc83..9c53c24b 100644 --- a/server/websocketserver.h +++ b/server/websocketserver.h @@ -22,6 +22,9 @@ #define WEBSOCKETSERVER_H #include +#include +#include +#include #include "transportinterface.h" @@ -35,11 +38,14 @@ class WebSocketServer : public TransportInterface Q_OBJECT public: explicit WebSocketServer(QObject *parent = 0); + ~WebSocketServer(); -signals: + void sendData(const QUuid &clientId, const QVariantMap &data) override; + void sendData(const QList &clients, const QVariantMap &data) override; public slots: - + bool startServer() override; + bool stopServer() override; }; }