/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2015 Simon Stuerz * * * * 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /*! \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(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(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(); } }