nymea-plugins/snapd/snapdconnection.cpp

387 lines
12 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "snapdconnection.h"
#include "extern-plugininfo.h"
#include <QPointer>
#include <QJsonDocument>
#include <QJsonParseError>
SnapdConnection::SnapdConnection(QObject *parent) :
QLocalSocket(parent)
{
connect(this, &QLocalSocket::connected, this, &SnapdConnection::onConnected);
connect(this, &QLocalSocket::disconnected, this, &SnapdConnection::onDisconnected);
connect(this, &QLocalSocket::readyRead, this, &SnapdConnection::onReadyRead);
connect(this, &QLocalSocket::stateChanged, this, &SnapdConnection::onStateChanged);
connect(this, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(onError(QLocalSocket::LocalSocketError)));
}
SnapdConnection::~SnapdConnection()
{
close();
}
SnapdReply *SnapdConnection::get(const QString &path, QObject *parent)
{
SnapdReply *reply = new SnapdReply(parent);
reply->setRequestPath(path);
reply->setRequestMethod("GET");
reply->setRequestRawMessage(createRequestHeader("GET", path));
// Enqueue the new reply
m_replyQueue.enqueue(reply);
sendNextRequest();
// Note: the caller owns the object now
return reply;
}
SnapdReply *SnapdConnection::post(const QString &path, const QByteArray &payload, QObject *parent)
{
SnapdReply *reply = new SnapdReply(parent);
reply->setRequestPath(path);
reply->setRequestMethod("POST");
QByteArray header = createRequestHeader("POST", path, payload);
reply->setRequestRawMessage(header.append(payload));
// Enqueue the new reply
m_replyQueue.enqueue(reply);
sendNextRequest();
// Note: the caller owns the object now
return reply;
}
SnapdReply *SnapdConnection::put(const QString &path, const QByteArray &payload, QObject *parent)
{
SnapdReply *reply = new SnapdReply(parent);
reply->setRequestPath(path);
reply->setRequestMethod("PUT");
QByteArray header = createRequestHeader("PUT", path, payload);
reply->setRequestRawMessage(header.append(payload));
// Enqueue the new reply
m_replyQueue.enqueue(reply);
sendNextRequest();
// Note: the caller owns the object now
return reply;
}
bool SnapdConnection::isConnected() const
{
return m_connected;
}
void SnapdConnection::setConnected(const bool &connected)
{
if (m_connected == connected)
return;
m_connected = connected;
emit connectedChanged(m_connected);
// Clean up replies of disconnected
if (!m_connected) {
// Clean up current reply
if (m_currentReply) {
m_currentReply->setFinished(false);
m_currentReply = nullptr;
}
// Clean up queue
while (!m_replyQueue.isEmpty()) {
QPointer<SnapdReply> reply = m_replyQueue.dequeue();
if (!reply.isNull()) {
reply->deleteLater();
}
}
} else {
// Start with a clean parsing
m_payload.clear();
m_header.clear();
m_chuncked = false;
}
}
QByteArray SnapdConnection::createRequestHeader(const QString &method, const QString &path, const QByteArray &payload)
{
QByteArray request;
request.append(QString("%1 %2 HTTP/1.1\r\n").arg(method).arg(path).toUtf8());
request.append("Host: http\r\n");
request.append("Accept: *\r\n");
if (!payload.isEmpty()) {
request.append("Content-Type: application/json\r\n");
request.append(QString("Content-Length: %1\r\n").arg(payload.count()).toUtf8());
}
request.append("\r\n");
return request;
}
QByteArray SnapdConnection::getChunckedPayload(const QByteArray &payload)
{
// Read line by line
QStringList payloadLines = QString::fromUtf8(payload).split(QRegExp("\r\n"));
if (payloadLines.count() < 4) {
qCWarning(dcSnapd()) << "Chuncked payload invalid linecount" << payloadLines.count();
return QByteArray();
}
int payloadSize = payloadLines.at(2).toInt(0, 16);
if (m_debug)
qCDebug(dcSnapd()) << "Payload size" << payloadSize;
if (payloadLines.at(3).toUtf8().size() != payloadSize) {
qCWarning(dcSnapd()) << "Invalid payload size" << payloadLines.at(3).toUtf8().size() << "!=" << payloadSize;
return QByteArray();
}
// Return just the payload
return payloadLines.at(3).toUtf8();
}
void SnapdConnection::processData()
{
if (!m_currentReply) {
qCWarning(dcSnapd()) << "Data received without current reply" << m_payload;
return;
}
if (m_header.isEmpty()) {
qCWarning(dcSnapd()) << "Could not process data. There is no header.";
m_currentReply->setFinished();
return;
}
// Get the raw payload
QByteArray payloadData;
if (m_chuncked) {
payloadData = getChunckedPayload(m_payload);
} else {
payloadData = m_payload;
}
// Check if there are data to process
if (m_payload.isEmpty()) {
qCWarning(dcSnapd()) << "Could not process data. There is no payload to process.";
return;
}
// Parse header
QHash<QString, QString> parsedHeader;
QStringList headerLines = QString::fromUtf8(m_header).split(QRegExp("\r\n"));
// Read status line
QString statusLine = headerLines.takeFirst();
QStringList statusLineTokens = statusLine.split(QRegExp("[ \r\n][ \r\n]*"));
if (statusLineTokens.count() < 3) {
qCWarning(dcSnapd()) << "Could not parse HTTP status line:" << statusLine;
return;
}
bool statusCodeOk = false;
int statusCode = statusLineTokens.at(1).simplified().toInt(&statusCodeOk);
if (!statusCodeOk) {
qCWarning(dcSnapd()) << "Could not parse HTTP status code:" << statusLineTokens.at(1);
return;
}
QString statusMessage;
for (int i = 2; i < statusLineTokens.count(); i++) {
statusMessage.append(statusLineTokens.at(i).simplified());
if (i < statusLineTokens.count() -1) {
statusMessage.append(" ");
}
}
// Verify header formating
foreach (const QString &line, headerLines) {
if (!line.contains(":")) {
qCWarning(dcSnapd()) << "Invalid HTTP header. Missing \":\" in line" << line;
return;
}
int index = line.indexOf(":");
QString key = line.left(index).toUtf8().simplified();
QString value = line.right(line.count() - index - 1).toUtf8().simplified();
//qCDebug(dcSnapd()) << " Key:" << key << "Value:" << value;
parsedHeader.insert(key, value);
}
// Parse payload
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(payloadData, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcSnapd()) << "Got invalid JSON data from snapd:" << error.offset << error.errorString();
qCWarning(dcSnapd()) << qUtf8Printable(payloadData);
return;
}
if (m_debug)
qCDebug(dcSnapd()) << "<--" << m_currentReply->requestPath() << statusCode << statusMessage;
// Fill reply
m_currentReply->setStatusCode(statusCode);
m_currentReply->setStatusMessage(statusMessage);
m_currentReply->setHeader(parsedHeader);
m_currentReply->setDataMap(jsonDoc.toVariant().toMap());
m_currentReply->setFinished();
// Current data stream finished, reset for new messages
m_payload.clear();
m_header.clear();
m_chuncked = false;
// Ready for next reply
m_currentReply = nullptr;
sendNextRequest();
}
void SnapdConnection::sendNextRequest()
{
// Check if nothing else to do
if (m_replyQueue.isEmpty())
return;
// Check a reply is currently pending
if (m_currentReply)
return;
// Dequeue and send next reply
SnapdReply *reply = m_replyQueue.dequeue();
m_currentReply = reply;
if (m_debug)
qCDebug(dcSnapd()) << "-->" << reply->requestMethod() << reply->requestPath();
// Send current reply request. If write failes, the reply is finished invalid and the owner has to delete it
qint64 bytesWritten = write(reply->requestRawMessage());
if (bytesWritten < 0) {
qCWarning(dcSnapd()) << "Could not write request data" << reply->requestMethod() << reply->requestMethod();
m_currentReply->setFinished(false);
m_currentReply = nullptr;
sendNextRequest();
}
}
void SnapdConnection::onConnected()
{
setConnected(true);
}
void SnapdConnection::onDisconnected()
{
setConnected(false);
}
void SnapdConnection::onError(const QLocalSocket::LocalSocketError &socketError)
{
qCWarning(dcSnapd()) << "Socket error" << socketError << errorString();
}
void SnapdConnection::onStateChanged(const QLocalSocket::LocalSocketState &state)
{
switch (state) {
case QLocalSocket::UnconnectedState:
qCDebug(dcSnapd()) << "Disconnected from snapd.";
break;
case QLocalSocket::ConnectingState:
qCDebug(dcSnapd()) << "Connecting to snapd...";
break;
case QLocalSocket::ConnectedState:
qCDebug(dcSnapd()) << "Connected to snapd.";
break;
case QLocalSocket::ClosingState:
qCDebug(dcSnapd()) << "Closing connection to snapd.";
break;
default:
break;
}
}
void SnapdConnection::onReadyRead()
{
QByteArray data = readAll();
if (m_debug)
qCDebug(dcSnapd()) << "Data received:" << data;
// If we are not appending to a reply
if (!m_chuncked) {
// Parse header
int headerIndex = data.indexOf("\r\n\r\n");
if (headerIndex < 0) {
qCWarning(dcSnapd()) << "Invalid response format. Could not find header/payload mark.";
return;
}
m_header = data.left(headerIndex);
if (m_debug)
qCDebug(dcSnapd()) << "Header:" << m_header;
QByteArray payload = data.right(data.length() - (headerIndex));
if (m_debug)
qCDebug(dcSnapd()) << "Payload" << payload;
// Check if this message is chuncked
if (m_header.contains("chunked")) {
if (m_debug)
qCDebug(dcSnapd()) << "Chuncked message receiving";
m_chuncked = true;
m_payload.append(payload);
if (m_payload.endsWith("\r\n0\r\n\r\n")) {
// Chuncked message finished
processData();
}
} else {
// Not chucked
m_payload = payload;
processData();
}
} else {
m_payload.append(data);
if (m_payload.endsWith("\r\n0\r\n\r\n")) {
// Chuncked message finished
processData();
}
}
}