/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 . * * 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 #include #include 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 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 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(); } } }