mirror of https://github.com/nymea/nymea.git
529 lines
18 KiB
C++
529 lines
18 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* *
|
|
* Copyright (C) 2015 Simon Stuerz <simon.stuerz@guh.guru> *
|
|
* *
|
|
* This file is part of QtCoap. *
|
|
* *
|
|
* QtCoap is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, version 3 of the License. *
|
|
* *
|
|
* QtCoap 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 QtCoap. If not, see <http://www.gnu.org/licenses/>. *
|
|
* *
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
/*!
|
|
\class Coap
|
|
\brief The client connection class to a CoAP server.
|
|
|
|
\ingroup coap
|
|
\inmodule libguh
|
|
|
|
The Coap class provides a signal solt based communication with a \l{https://tools.ietf.org/html/rfc7252}{CoAP (Constrained Application Protocol)}
|
|
server. The API of this class was inspired by the \l{http://doc.qt.io/qt-5/qnetworkaccessmanager.html}{QNetworkAccessManager} and was
|
|
written according to the \l{https://tools.ietf.org/html/rfc7252}{RFC7252}.
|
|
This class supports also blockwise transfere according to the \l{https://tools.ietf.org/html/draft-ietf-core-block-18}{IETF V18} specifications.
|
|
|
|
\sa CoapReply, CoapRequest
|
|
|
|
\section2 Example
|
|
\code
|
|
MyClass::MyClass(QObject *parent) :
|
|
QObject(parent)
|
|
{
|
|
Coap *coap = new Coap(this);
|
|
connect(coap, SIGNAL(replyFinished(CoapReply*)), this, SLOT(onReplyFinished(CoapReply*)));
|
|
|
|
CoapRequest request(QUrl("coap://coap.me/hello"));
|
|
coap->get(request);
|
|
}
|
|
\endcode
|
|
|
|
\code
|
|
void MyClass::onReplyFinished(CoapReply *reply)
|
|
{
|
|
if (reply->error() != CoapReply::NoError) {
|
|
qWarning() << "Reply finished with error" << reply->errorString();
|
|
}
|
|
|
|
qDebug() << "Reply finished" << reply;
|
|
reply->deleteLater();
|
|
}
|
|
\endcode
|
|
*/
|
|
|
|
/*! \fn void Coap::replyFinished(CoapReply *reply);
|
|
This signal is emitted when a \a reply is finished.
|
|
*/
|
|
|
|
#include "coap.h"
|
|
#include "coappdu.h"
|
|
#include "coapoption.h"
|
|
|
|
/*! Constructs a coap access manager with the given \a parent and \a port. */
|
|
Coap::Coap(QObject *parent, const quint16 &port) :
|
|
QObject(parent),
|
|
m_reply(0)
|
|
{
|
|
m_socket = new QUdpSocket(this);
|
|
|
|
if (!m_socket->bind(QHostAddress::Any, port))
|
|
qWarning() << "Could not bind to port" << port << m_socket->errorString();
|
|
|
|
connect(m_socket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
|
|
}
|
|
|
|
/*! Performs a ping request to the CoAP server specified in the given \a request.
|
|
* Returns a \l{CoapReply} to match the response with the request. */
|
|
CoapReply *Coap::ping(const CoapRequest &request)
|
|
{
|
|
CoapReply *reply = new CoapReply(request, this);
|
|
reply->setRequestMethod(CoapPdu::Empty);
|
|
|
|
connect(reply, &CoapReply::timeout, this, &Coap::onReplyTimeout);
|
|
connect(reply, &CoapReply::finished, this, &Coap::onReplyFinished);
|
|
|
|
if (request.url().scheme() != "coap") {
|
|
reply->setError(CoapReply::InvalidUrlSchemeError);
|
|
reply->m_isFinished = true;
|
|
return reply;
|
|
}
|
|
|
|
// check if there is a request running
|
|
if (m_reply == 0) {
|
|
m_reply = reply;
|
|
lookupHost();
|
|
} else {
|
|
m_replyQueue.enqueue(reply);
|
|
}
|
|
|
|
return reply;
|
|
}
|
|
|
|
/*! Performs a GET request to the CoAP server specified in the given \a request.
|
|
* Returns a \l{CoapReply} to match the response with the request. */
|
|
CoapReply *Coap::get(const CoapRequest &request)
|
|
{
|
|
CoapReply *reply = new CoapReply(request, this);
|
|
reply->setRequestMethod(CoapPdu::Get);
|
|
|
|
connect(reply, &CoapReply::timeout, this, &Coap::onReplyTimeout);
|
|
connect(reply, &CoapReply::finished, this, &Coap::onReplyFinished);
|
|
|
|
if (request.url().scheme() != "coap") {
|
|
reply->setError(CoapReply::InvalidUrlSchemeError);
|
|
reply->m_isFinished = true;
|
|
return reply;
|
|
}
|
|
|
|
// check if there is a request running
|
|
if (m_reply == 0) {
|
|
m_reply = reply;
|
|
lookupHost();
|
|
} else {
|
|
m_replyQueue.enqueue(reply);
|
|
}
|
|
|
|
return reply;
|
|
}
|
|
|
|
/*! Performs a PUT request to the CoAP server specified in the given \a request and \a data.
|
|
* Returns a \l{CoapReply} to match the response with the request. */
|
|
CoapReply *Coap::put(const CoapRequest &request, const QByteArray &data)
|
|
{
|
|
CoapReply *reply = new CoapReply(request, this);
|
|
reply->setRequestMethod(CoapPdu::Put);
|
|
reply->setRequestPayload(data);
|
|
|
|
connect(reply, &CoapReply::timeout, this, &Coap::onReplyTimeout);
|
|
connect(reply, &CoapReply::finished, this, &Coap::onReplyFinished);
|
|
|
|
if (request.url().scheme() != "coap") {
|
|
reply->setError(CoapReply::InvalidUrlSchemeError);
|
|
reply->m_isFinished = true;
|
|
return reply;
|
|
}
|
|
|
|
// check if there is a request running
|
|
if (m_reply == 0) {
|
|
m_reply = reply;
|
|
lookupHost();
|
|
} else {
|
|
m_replyQueue.enqueue(reply);
|
|
}
|
|
|
|
return reply;
|
|
}
|
|
|
|
/*! Performs a POST request to the CoAP server specified in the given \a request and \a data.
|
|
* Returns a \l{CoapReply} to match the response with the request. */
|
|
CoapReply *Coap::post(const CoapRequest &request, const QByteArray &data)
|
|
{
|
|
CoapReply *reply = new CoapReply(request, this);
|
|
reply->setRequestMethod(CoapPdu::Post);
|
|
reply->setRequestPayload(data);
|
|
|
|
connect(reply, &CoapReply::timeout, this, &Coap::onReplyTimeout);
|
|
connect(reply, &CoapReply::finished, this, &Coap::onReplyFinished);
|
|
|
|
if (request.url().scheme() != "coap") {
|
|
reply->setError(CoapReply::InvalidUrlSchemeError);
|
|
reply->m_isFinished = true;
|
|
return reply;
|
|
}
|
|
|
|
// check if there is a request running
|
|
if (m_reply == 0) {
|
|
m_reply = reply;
|
|
lookupHost();
|
|
} else {
|
|
m_replyQueue.enqueue(reply);
|
|
}
|
|
|
|
return reply;
|
|
}
|
|
|
|
/*! Performs a DELETE request to the CoAP server specified in the given \a request.
|
|
* Returns a \l{CoapReply} to match the response with the request. */
|
|
CoapReply *Coap::deleteResource(const CoapRequest &request)
|
|
{
|
|
CoapReply *reply = new CoapReply(request, this);
|
|
reply->setRequestMethod(CoapPdu::Delete);
|
|
|
|
connect(reply, &CoapReply::timeout, this, &Coap::onReplyTimeout);
|
|
connect(reply, &CoapReply::finished, this, &Coap::onReplyFinished);
|
|
|
|
if (request.url().scheme() != "coap") {
|
|
reply->setError(CoapReply::InvalidUrlSchemeError);
|
|
reply->m_isFinished = true;
|
|
return reply;
|
|
}
|
|
|
|
// check if there is a request running
|
|
if (m_reply == 0) {
|
|
m_reply = reply;
|
|
lookupHost();
|
|
} else {
|
|
m_replyQueue.enqueue(reply);
|
|
}
|
|
|
|
return reply;
|
|
}
|
|
|
|
void Coap::lookupHost()
|
|
{
|
|
int lookupId = QHostInfo::lookupHost(m_reply->request().url().host(), this, SLOT(hostLookupFinished(QHostInfo)));
|
|
m_runningHostLookups.insert(lookupId, m_reply);
|
|
}
|
|
|
|
void Coap::sendRequest(CoapReply *reply, const bool &lookedUp)
|
|
{
|
|
CoapPdu pdu;
|
|
pdu.setMessageType(reply->request().messageType());
|
|
pdu.setStatusCode(reply->requestMethod());
|
|
pdu.createMessageId();
|
|
pdu.createToken();
|
|
|
|
if (lookedUp)
|
|
pdu.addOption(CoapOption::UriHost, reply->request().url().host().toUtf8());
|
|
|
|
QStringList urlTokens = reply->request().url().path().split("/");
|
|
urlTokens.removeAll(QString());
|
|
|
|
foreach (const QString &token, urlTokens)
|
|
pdu.addOption(CoapOption::UriPath, token.toUtf8());
|
|
|
|
if (reply->request().url().hasQuery())
|
|
pdu.addOption(CoapOption::UriQuery, reply->request().url().query().toUtf8());
|
|
|
|
if (reply->requestMethod() == CoapPdu::Get)
|
|
pdu.addOption(CoapOption::Block2, CoapPduBlock::createBlock(0));
|
|
|
|
if (reply->requestMethod() == CoapPdu::Post || reply->requestMethod() == CoapPdu::Put) {
|
|
pdu.addOption(CoapOption::ContentFormat, QByteArray(1, ((quint8)reply->request().contentType())));
|
|
|
|
// check if we have to block the payload
|
|
if (reply->requestPayload().size() > 64) {
|
|
pdu.addOption(CoapOption::Block1, CoapPduBlock::createBlock(0, 2, true));
|
|
pdu.setPayload(reply->requestPayload().mid(0, 64));
|
|
} else {
|
|
pdu.setPayload(reply->requestPayload());
|
|
}
|
|
}
|
|
|
|
QByteArray pduData = pdu.pack();
|
|
reply->setRequestData(pduData);
|
|
reply->setMessageId(pdu.messageId());
|
|
reply->setMessageToken(pdu.token());
|
|
reply->m_lockedUp = lookedUp;
|
|
reply->m_timer->start();
|
|
|
|
qDebug() << "--->" << pdu;
|
|
|
|
// send the data
|
|
if (reply->request().messageType() == CoapPdu::NonConfirmable) {
|
|
sendData(reply->hostAddress(), reply->port(), pduData);
|
|
reply->setFinished();
|
|
} else {
|
|
sendData(reply->hostAddress(), reply->port(), pduData);
|
|
}
|
|
}
|
|
|
|
void Coap::sendData(const QHostAddress &hostAddress, const quint16 &port, const QByteArray &data)
|
|
{
|
|
m_socket->writeDatagram(data, hostAddress, port);
|
|
}
|
|
|
|
void Coap::sendCoapPdu(const QHostAddress &hostAddress, const quint16 &port, const CoapPdu &pdu)
|
|
{
|
|
qDebug() << "--->" << pdu;
|
|
m_socket->writeDatagram(pdu.pack(), hostAddress, port);
|
|
}
|
|
|
|
void Coap::processResponse(const CoapPdu &pdu)
|
|
{
|
|
qDebug() << "<---" << pdu;
|
|
|
|
if (!pdu.isValid()) {
|
|
qWarning() << "Got invalid PDU";
|
|
m_reply->setError(CoapReply::InvalidPduError);
|
|
m_reply->setFinished();
|
|
return;
|
|
}
|
|
|
|
// check if the message is a response to a reply (message id based check)
|
|
if (m_reply->messageId() == pdu.messageId()) {
|
|
processIdBasedResponse(m_reply, pdu);
|
|
return;
|
|
}
|
|
|
|
// check if we know the message by token (message token based check)
|
|
if (m_reply->messageToken() == pdu.token()) {
|
|
processTokenBasedResponse(m_reply, pdu);
|
|
return;
|
|
}
|
|
|
|
qDebug() << "Got message without request";
|
|
}
|
|
|
|
void Coap::processIdBasedResponse(CoapReply *reply, const CoapPdu &pdu)
|
|
{
|
|
// check if this is an empty ACK response (which indicates a separated response)
|
|
if (pdu.statusCode() == CoapPdu::Empty && pdu.messageType() == CoapPdu::Acknowledgement) {
|
|
reply->m_timer->stop();
|
|
qDebug() << "Got empty ACK. Data will be sent separated.";
|
|
return;
|
|
}
|
|
|
|
// check if this is a Block1 pdu
|
|
if (pdu.messageType() == CoapPdu::Acknowledgement && pdu.hasOption(CoapOption::Block1)) {
|
|
processBlock1Response(reply, pdu);
|
|
return;
|
|
}
|
|
|
|
// check if this is a Block2 pdu
|
|
if (pdu.messageType() == CoapPdu::Acknowledgement && pdu.hasOption(CoapOption::Block2)) {
|
|
processBlock2Response(reply, pdu);
|
|
return;
|
|
}
|
|
|
|
// Piggybacked response
|
|
reply->setStatusCode(pdu.statusCode());
|
|
reply->setContentType(pdu.contentType());
|
|
reply->appendPayloadData(pdu.payload());
|
|
reply->setFinished();
|
|
}
|
|
|
|
void Coap::processTokenBasedResponse(CoapReply *reply, const CoapPdu &pdu)
|
|
{
|
|
// Separate Response
|
|
CoapPdu responsePdu;
|
|
responsePdu.setMessageType(CoapPdu::Acknowledgement);
|
|
responsePdu.setStatusCode(CoapPdu::Empty);
|
|
responsePdu.setMessageId(pdu.messageId());
|
|
sendCoapPdu(reply->hostAddress(), reply->port(), responsePdu);
|
|
|
|
reply->setStatusCode(pdu.statusCode());
|
|
reply->setContentType(pdu.contentType());
|
|
reply->appendPayloadData(pdu.payload());
|
|
reply->setFinished();
|
|
}
|
|
|
|
void Coap::processBlock1Response(CoapReply *reply, const CoapPdu &pdu)
|
|
{
|
|
qDebug() << "sent successfully block #" << pdu.block().blockNumber();
|
|
|
|
// create next block
|
|
int index = (pdu.block().blockNumber() * 64) + 64;
|
|
QByteArray newBlockData = reply->requestPayload().mid(index, 64);
|
|
bool moreFlag = true;
|
|
|
|
// check if this was the last block
|
|
if (newBlockData.isEmpty()) {
|
|
reply->setStatusCode(pdu.statusCode());
|
|
reply->setContentType(pdu.contentType());
|
|
reply->setFinished();
|
|
return;
|
|
}
|
|
|
|
// check if this is the last block or there will be no next block
|
|
if (newBlockData.size() < 64 || (index + 64) == reply->requestPayload().size())
|
|
moreFlag = false;
|
|
|
|
CoapPdu nextBlockRequest;
|
|
nextBlockRequest.setContentType(reply->request().contentType());
|
|
nextBlockRequest.setMessageType(reply->request().messageType());
|
|
nextBlockRequest.setStatusCode(reply->requestMethod());
|
|
nextBlockRequest.setMessageId(pdu.messageId() + 1);
|
|
nextBlockRequest.setToken(pdu.token());
|
|
|
|
if (reply->m_lockedUp)
|
|
nextBlockRequest.addOption(CoapOption::UriHost, reply->request().url().host().toUtf8());
|
|
|
|
if (reply->port() != 5683)
|
|
nextBlockRequest.addOption(CoapOption::UriPort, QByteArray::number(reply->request().url().port()));
|
|
|
|
QStringList urlTokens = reply->request().url().path().split("/");
|
|
urlTokens.removeAll(QString());
|
|
|
|
foreach (const QString &token, urlTokens)
|
|
nextBlockRequest.addOption(CoapOption::UriPath, token.toUtf8());
|
|
|
|
if (reply->request().url().hasQuery())
|
|
nextBlockRequest.addOption(CoapOption::UriQuery, reply->request().url().query().toUtf8());
|
|
|
|
nextBlockRequest.addOption(CoapOption::Block1, CoapPduBlock::createBlock(pdu.block().blockNumber() + 1, 2, moreFlag));
|
|
|
|
nextBlockRequest.setPayload(newBlockData);
|
|
|
|
QByteArray pduData = nextBlockRequest.pack();
|
|
reply->setRequestData(pduData);
|
|
reply->m_timer->start();
|
|
reply->m_retransmissions = 1;
|
|
|
|
reply->setMessageId(nextBlockRequest.messageId());
|
|
|
|
qDebug() << "--->" << nextBlockRequest;
|
|
sendData(reply->hostAddress(), reply->port(), pduData);
|
|
}
|
|
|
|
void Coap::processBlock2Response(CoapReply *reply, const CoapPdu &pdu)
|
|
{
|
|
reply->appendPayloadData(pdu.payload());
|
|
|
|
// check if this was the last block
|
|
if (!pdu.block().moreFlag()) {
|
|
reply->setStatusCode(pdu.statusCode());
|
|
reply->setContentType(pdu.contentType());
|
|
reply->setFinished();
|
|
return;
|
|
}
|
|
|
|
CoapPdu nextBlockRequest;
|
|
nextBlockRequest.setContentType(reply->request().contentType());
|
|
nextBlockRequest.setMessageType(reply->request().messageType());
|
|
nextBlockRequest.setStatusCode(reply->requestMethod());
|
|
nextBlockRequest.setMessageId(pdu.messageId() + 1);
|
|
nextBlockRequest.setToken(pdu.token());
|
|
|
|
if (reply->m_lockedUp)
|
|
nextBlockRequest.addOption(CoapOption::UriHost, reply->request().url().host().toUtf8());
|
|
|
|
if (reply->port() != 5683)
|
|
nextBlockRequest.addOption(CoapOption::UriPort, QByteArray::number(reply->request().url().port()));
|
|
|
|
|
|
QStringList urlTokens = reply->request().url().path().split("/");
|
|
urlTokens.removeAll(QString());
|
|
|
|
foreach (const QString &token, urlTokens)
|
|
nextBlockRequest.addOption(CoapOption::UriPath, token.toUtf8());
|
|
|
|
if (reply->request().url().hasQuery())
|
|
nextBlockRequest.addOption(CoapOption::UriQuery, reply->request().url().query().toUtf8());
|
|
|
|
nextBlockRequest.addOption(CoapOption::Block2, CoapPduBlock::createBlock(pdu.block().blockNumber() + 1, 2, false));
|
|
|
|
QByteArray pduData = nextBlockRequest.pack();
|
|
reply->setRequestData(pduData);
|
|
reply->m_timer->start();
|
|
|
|
reply->setMessageId(nextBlockRequest.messageId());
|
|
|
|
qDebug() << "--->" << nextBlockRequest;
|
|
sendData(reply->hostAddress(), reply->port(), pduData);
|
|
}
|
|
|
|
void Coap::hostLookupFinished(const QHostInfo &hostInfo)
|
|
{
|
|
CoapReply *reply = m_runningHostLookups.take(hostInfo.lookupId());;
|
|
reply->setPort(reply->request().url().port(5683));
|
|
|
|
if (hostInfo.error() != QHostInfo::NoError) {
|
|
qDebug() << "Host lookup for" << reply->request().url().host() << "failed:" << hostInfo.errorString();
|
|
reply->setError(CoapReply::HostNotFoundError);
|
|
reply->setFinished();
|
|
return;
|
|
}
|
|
|
|
QHostAddress hostAddress = hostInfo.addresses().first();
|
|
reply->setHostAddress(hostAddress);
|
|
|
|
// check if the url had to be looked up
|
|
if (reply->request().url().host() != hostAddress.toString()) {
|
|
qDebug() << reply->request().url().host() << " -> " << hostAddress.toString();
|
|
sendRequest(reply, true);
|
|
} else {
|
|
sendRequest(reply);
|
|
}
|
|
}
|
|
|
|
void Coap::onReadyRead()
|
|
{
|
|
QHostAddress hostAddress;
|
|
QByteArray data;
|
|
quint16 port;
|
|
|
|
while (m_socket->hasPendingDatagrams()) {
|
|
data.resize(m_socket->pendingDatagramSize());
|
|
m_socket->readDatagram(data.data(), data.size(), &hostAddress, &port);
|
|
}
|
|
|
|
CoapPdu pdu(data);
|
|
processResponse(pdu);
|
|
}
|
|
|
|
void Coap::onReplyTimeout()
|
|
{
|
|
CoapReply *reply = qobject_cast<CoapReply *>(sender());
|
|
if (reply->m_retransmissions < 5) {
|
|
qDebug() << QString("Reply timeout: resending message %1/4").arg(reply->m_retransmissions);
|
|
}
|
|
reply->resend();
|
|
m_socket->writeDatagram(reply->requestData(), reply->hostAddress(), reply->port());
|
|
}
|
|
|
|
void Coap::onReplyFinished()
|
|
{
|
|
CoapReply *reply = qobject_cast<CoapReply *>(sender());
|
|
|
|
if (reply != m_reply)
|
|
qWarning() << "This should never happen!! Please report a bug if you get this message!";
|
|
|
|
emit replyFinished(reply);
|
|
|
|
m_reply = 0;
|
|
// check if there is a request in the queue
|
|
if (!m_replyQueue.isEmpty()) {
|
|
m_reply = m_replyQueue.dequeue();
|
|
if (m_reply)
|
|
lookupHost();
|
|
}
|
|
}
|