/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qupnpservice.h" #include "qupnprootdevice.h" QUPnPService::QUPnPService(QObject *parent, QString identifier) : QObject(parent) { this->identifier = identifier; } QUPnPService::~QUPnPService() { } QString QUPnPService::upnpServiceListEntry() { return QStringLiteral( "" "urn:schemas-upnp-org:service:") + identifier + ":1" //"urn:upnp-org:serviceId:" + identifier + ":1" "" + identifier + "" "/" + identifier + "/scpd.xml" "/" + identifier + "/Control" "/" + identifier + "/Events" ""; } int QUPnPService::executeRemoteProcedureCall(QString actionName, QMap &actionParams, QString &responseHead, QByteArray &responseBody) { Q_UNUSED(actionName); Q_UNUSED(actionParams); Q_UNUSED(responseHead); Q_UNUSED(responseBody); responseHead = "HTTP/1.0 404 Not Found\r\n"; return 404; } int QUPnPService::processHttpSubscribeRequest(QStringList &lines, QString &responseHead, QByteArray &responseBody) { Q_UNUSED(responseBody); #ifdef QT_DEBUG qDebug() << "QUPnPService::processHttpSubscribeRequest"; #endif QUPnPServiceSubscription subscription; QString timeout = "0"; for (int i = 1; i < lines.size(); i++) { if (lines.at(i).startsWith("CALLBACK:", Qt::CaseInsensitive)) { QString url = lines.at(i).mid(9).trimmed(); if (url.startsWith("<") && url.endsWith(">")) url = url.mid(1, url.size()-2); subscription.url = QUrl(url); } else if (lines.at(i).startsWith("TIMEOUT:", Qt::CaseInsensitive)) { timeout = lines.at(i).mid(8).trimmed(); } else if (lines.at(i).startsWith("SID:", Qt::CaseInsensitive)) { // renewing a subscription subscription.uuid = lines.at(i).mid(4).trimmed(); } } if (timeout.startsWith("SECOND-", Qt::CaseInsensitive)) timeout = timeout.mid(7).trimmed(); bool ok = false; int iTimeout = timeout.toInt(&ok); subscription.timeoutTime = QDateTime::currentDateTime().addSecs(iTimeout); if (!ok || !subscription.url.isValid()) { responseHead = "HTTP/1.0 400 Bad Request\r\n"; return 400; } // no SID header => new subscription => new SID if (subscription.uuid == "") { subscription.uuid = QUuid::createUuid().toString(); subscription.uuid = QStringLiteral("uuid:") + subscription.uuid.mid(1, subscription.uuid.size()-2); subscriptions[subscription.uuid] = subscription; // delay the notification so that the "HTTP/1.1 200 OK" message comes first QString uuid = subscription.uuid; QTimer::singleShot(50, [uuid, this](){ if (subscriptions.contains(uuid)) onNewSubscriber(subscriptions[uuid]); }); } else { // only update the timeout. seq and url must stay the same, uuid would stay the same anyways if (subscriptions.contains(subscription.uuid)) { responseHead = "HTTP/1.0 412 Precondition Failed\r\n"; return 412; } subscriptions[subscription.uuid].timeoutTime = subscription.timeoutTime; } responseHead = responseHead + "SERVER: " + QSysInfo::productType() + "/" + QSysInfo::productVersion() + " UPnP/1.0 " + rootdevice->product + "/" + rootdevice->version + "\r\n" + "SID: " + subscription.uuid + "\r\n" + "TIMEOUT: Second-" + QString::number(iTimeout) + "\r\n"; return 200; } int QUPnPService::processHttpUnsubscribeRequest(QStringList &lines, QString &responseHead, QByteArray &responseBody) { Q_UNUSED(responseBody); #ifdef QT_DEBUG qDebug() << "QUPnPService::processHttpUnsubscribeRequest"; #endif // find SID: header QString SubscriptionUuid; for (int i = 1; i < lines.size(); i++) if (lines.at(i).startsWith("SID:", Qt::CaseInsensitive)) SubscriptionUuid = lines.at(i).mid(4).trimmed(); if (SubscriptionUuid == "" || !subscriptions.contains(SubscriptionUuid)) { responseHead = "HTTP/1.0 412 Precondition Failed\r\n"; return 412; } subscriptions.remove(SubscriptionUuid); return 200; } bool QUPnPService::notifySubscriber(QUPnPServiceSubscription &subscription, const QString &propertyName, const QString &propertyValue) { QByteArray notificationBody = (QStringLiteral( "" "" "<") + propertyName + ">" + propertyValue + "" "" "").toUtf8(); QString notification = QStringLiteral( "NOTIFY ") + subscription.url.path() + " HTTP/1.1\r\n" "HOST: " + subscription.url.host(); if (subscription.url.port() > 0) notification += QStringLiteral(":") + QString::number(subscription.url.port()); notification += "\r\n" "CONTENT-TYPE: text/xml; charset=utf-8\r\n" "NT: upnp:event\r\n" "NTS: upnp:propchange\r\n" "CONTENT-LENGTH: " + QString::number(notificationBody.size()) + "\r\n" "SID: " + subscription.uuid + "\r\n" "SEQ: " + QString::number(subscription.sequenceNumber) + "\r\n" "CONNECTION: close\r\n" "\r\n"; if (subscription.sequenceNumber >= UINT_MAX) subscription.sequenceNumber = 1; // not 0! else subscription.sequenceNumber++; QByteArray message = notification.toUtf8(); message.append(notificationBody); #ifdef QT_DEBUG qDebug() << "QUPnPService::notifySubscriber: " << message; #endif QTcpSocket notificationSocket; notificationSocket.connectToHost(subscription.url.host(), subscription.url.port(80)); if (!notificationSocket.waitForConnected(10000)) { qDebug() << "QUPnPService::notifySubscriber: ERROR could not connect to host"; return false; } notificationSocket.write(message); notificationSocket.waitForReadyRead(30000); // See UPnP specification, chapter 4.2.1 #ifdef QT_DEBUG qDebug() << "QUPnPService::notifySubscriber RESPONSE: " << message; #endif return true; } void QUPnPService::notifyPropertyChanged(const QString &propertyName, const QString &propertyValue) { #ifdef QT_DEBUG qDebug() << "QUPnPService::notifyPropertyChanged"; #endif QDateTime now = QDateTime::currentDateTime(); QList expiredSubscriptions; for (QString subscriptionKey : subscriptions.keys()) { QUPnPServiceSubscription &subscription = subscriptions[subscriptionKey]; bool mustRemove = true; if (now <= subscription.timeoutTime) mustRemove = !notifySubscriber(subscription, propertyName, propertyValue); if (mustRemove) expiredSubscriptions.push_back(subscriptionKey); } for (QString expiredKey : expiredSubscriptions) subscriptions.remove(expiredKey); } QUPnPBooleanService::QUPnPBooleanService(QObject *parent, QString identifier) : QUPnPService(parent, identifier) { } bool QUPnPBooleanService::doSetState(bool state) { // if (state == this->state) // return false; this->state = state; return true; } void QUPnPBooleanService::setState(bool state) { if (doSetState(state)) { notifyPropertyChanged("state", state?"1":"0"); emit this->stateChanged(state); } else { emit this->stateChangeFailed(); } } bool QUPnPBooleanService::getState() { return state; } QString QUPnPBooleanService::getScpd() { return QStringLiteral( "" "" "1" "0" "" "" "" "setState" "" "" "newState" "state" "in" "" "" "" "" "" "" "state" "boolean" "") + (getState()?"1":"0") + "" "" "" ""; } int QUPnPBooleanService::executeRemoteProcedureCall(QString actionName, QMap &actionParams, QString &responseHead, QByteArray &responseBody) { #ifdef QT_DEBUG qDebug() << "QUPnPBooleanService::executeRemoteProcedureCall"; #endif bool ok = false; int newStateValue = actionParams["newState"].toInt(&ok); if (actionName.toUpper() != "SETSTATE" || newStateValue < 0 || 1 < newStateValue || !ok) { responseHead = "HTTP/1.0 400 Bad Request\r\n"; return 400; } this->setState(newStateValue == 1); responseHead = QStringLiteral( "HTTP/1.0 200 OK\r\n" "CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n"); responseBody = (QStringLiteral( "" "" "" "" "" "").toUtf8(); return 200; } void QUPnPBooleanService::onNewSubscriber(QUPnPServiceSubscription &subscription) { this->notifySubscriber(subscription, "state", getState()?"1":"0"); } QUPnPBooleanServiceProxy::QUPnPBooleanServiceProxy(QObject *parent, QUPnPBooleanService *target, QString identifier) : QUPnPBooleanService(parent, identifier) , m_target(target) { if (target) { connect(target, &QUPnPBooleanService::stateChanged, [this](bool state){ this->notifyPropertyChanged("state", state?"1":"0"); emit stateChanged(state); }); connect(target, &QUPnPBooleanService::stateChangeFailed, [this](){ emit stateChangeFailed(); }); } } void QUPnPBooleanServiceProxy::setState(bool state) { if (m_target) m_target->setState(state); } bool QUPnPBooleanServiceProxy::getState() { if (!m_target) return false; return m_target->getState(); }