快速安全CoAP客户端

确保CoAP客户端的安全并使用Qt Quick用户界面。

快速安全CoAP客户端展示了如何创建一个安全的CoAP客户端并在Qt Quick应用程序中使用它。

注意:当前版本的Qt CoAP不提供QML API。但是,您可以根据示例将其模块的C++类提供给QML。

运行示例

要从Qt Creator中运行示例,请打开欢迎模式并从示例中选择示例。有关更多信息,请访问构建和运行示例

要运行示例应用程序,您需要首先设置一个安全的CoAP服务器。您可以使用支持以下任一预共享密钥(PSK)证书认证模式的任何安全CoAP服务器来运行示例。有关设置安全CoAP服务器的更多信息,请参阅设置安全CoAP服务器

将C++类公开给QML

在此示例中,您需要将QCoapClient类和QtCoap命名空间公开给QML。要实现此目的,请创建一个自定义包装类并使用特殊的注册宏

创建QmlCoapSecureClient类作为QCoapClient的包装器。此类还包含所选的安全模式和安全性配置参数。使用Q_INVOKABLE宏将几个方法公开给QML。还使用QML_NAMED_ELEMENT宏将类在QML中注册为CoapSecureClient

class QmlCoapSecureClient : public QObject
{
    Q_OBJECT
    QML_NAMED_ELEMENT(CoapSecureClient)

public:
    QmlCoapSecureClient(QObject *parent = nullptr);
    ~QmlCoapSecureClient() override;

    Q_INVOKABLE void setSecurityMode(QtCoap::SecurityMode mode);
    Q_INVOKABLE void sendGetRequest(const QString &host, const QString &path, int port);
    Q_INVOKABLE void setSecurityConfiguration(const QString &preSharedKey, const QString &identity);
    Q_INVOKABLE void setSecurityConfiguration(const QString &localCertificatePath,
                                              const QString &caCertificatePath,
                                              const QString &privateKeyPath);
    Q_INVOKABLE void disconnect();

Q_SIGNALS:
    void finished(const QString &result);

private:
    QCoapClient *m_coapClient;
    QCoapSecurityConfiguration m_configuration;
    QtCoap::SecurityMode m_securityMode;
};

之后,注册QtCoap命名空间,因此您可以使用那里提供的枚举值。

namespace QCoapForeignNamespace
{
    Q_NAMESPACE
    QML_FOREIGN_NAMESPACE(QtCoap)
    QML_NAMED_ELEMENT(QtCoap)
}

调整构建文件

要使自定义类型从QML可用,请相应地更新构建系统文件。

CMake

对于基于CMake的构建,将以下内容添加到CMakeLists.txt中:

qt_add_qml_module(quicksecureclient
    URI CoapSecureClientModule
    SOURCES
        qmlcoapsecureclient.cpp qmlcoapsecureclient.h
    QML_FILES
        FilePicker.qml
        Main.qml
)
qmake

对于qmake构建,以下方式修改quicksecureclient.pro文件:

CONFIG += qmltypes
QML_IMPORT_NAME = CoapSecureClientModule
QML_IMPORT_MAJOR_VERSION = 1
    ...
qml_resources.files = \
    qmldir \
    FilePicker.qml \
    Main.qml

qml_resources.prefix = /qt/qml/CoapSecureClientModule

RESOURCES += qml_resources

使用新的QML类型

现在,当C++类正确公开给QML时,您可以使用新类型。

创建客户端

CoapSecureClient 对象从 Main.qml 文件实例化。它处理 QmlCoapSecureClient::finished() 信号并根据情况更新 UI

CoapSecureClient {
    id: client
    onFinished: (result) => {
        outputView.text = result;
        statusLabel.text = "";
        disconnectButton.enabled = true;
    }
}

当用户在 UI 中选择或更改安全模式时,会创建 QCoapClient 实例。当选中某个安全模式时,从 QML 代码调用 QmlCoapSecureClient::setSecurityMode() 方法

ButtonGroup {
    id: securityModeGroup
    onClicked: {
        if ((securityModeGroup.checkedButton as RadioButton) === preSharedMode)
            client.setSecurityMode(QtCoap.SecurityMode.PreSharedKey);
        else
            client.setSecurityMode(QtCoap.SecurityMode.Certificate);
    }
}

在 C++ 一侧,此方法创建一个 QCoapClient 并连接到其 finished() 和 error() 信号。该类内部处理这两个信号,并将它们转发到新的 finished() 信号。

void QmlCoapSecureClient::setSecurityMode(QtCoap::SecurityMode mode)
{
    // Create a new client, if the security mode has changed
    if (m_coapClient && mode != m_securityMode) {
        delete m_coapClient;
        m_coapClient = nullptr;
    }

    if (!m_coapClient) {
        m_coapClient = new QCoapClient(mode);
        m_securityMode = mode;

        connect(m_coapClient, &QCoapClient::finished, this,
                [this](QCoapReply *reply) {
                    if (!reply)
                        emit finished(tr("Something went wrong, received a null reply"));
                    else if (reply->errorReceived() != QtCoap::Error::Ok)
                        emit finished(errorMessage(reply->errorReceived()));
                    else
                        emit finished(reply->message().payload());
                });

        connect(m_coapClient, &QCoapClient::error, this,
                [this](QCoapReply *, QtCoap::Error errorCode) {
                    emit finished(errorMessage(errorCode));
                });
    }
}
发送请求

单击 发送请求 按钮根据所选安全模式设置安全配置并发送 GET 请求

Button {
    id: requestButton
    text: qsTr("Send Request")
    enabled: securityModeGroup.checkState !== Qt.Unchecked

    onClicked: {
        outputView.text = "";
        if ((securityModeGroup.checkedButton as RadioButton) === preSharedMode)
            client.setSecurityConfiguration(pskField.text, identityField.text);
        else
            client.setSecurityConfiguration(localCertificatePicker.selectedFile,
                                            caCertificatePicker.selectedFile,
                                            privateKeyPicker.selectedFile);

        client.sendGetRequest(hostComboBox.editText, resourceField.text,
                              parseInt(portField.text));

        statusLabel.text = qsTr("Sending request to %1%2...").arg(hostComboBox.editText)
                                                             .arg(resourceField.text);
    }
}

对于 setSecurityConfiguration 方法有两种重载方式。

PSK 模式重载仅设置客户端标识和预共享密钥

void
QmlCoapSecureClient::setSecurityConfiguration(const QString &preSharedKey, const QString &identity)
{
    QCoapSecurityConfiguration configuration;
    configuration.setPreSharedKey(preSharedKey.toUtf8());
    configuration.setPreSharedKeyIdentity(identity.toUtf8());
    m_configuration = configuration;
}

而 X.509 证书的重载读取证书文件和私钥,并设置安全配置

void QmlCoapSecureClient::setSecurityConfiguration(const QString &localCertificatePath,
                                                   const QString &caCertificatePath,
                                                   const QString &privateKeyPath)
{
    QCoapSecurityConfiguration configuration;

    const auto localCerts =
            QSslCertificate::fromPath(QUrl(localCertificatePath).toLocalFile(), QSsl::Pem,
                                      QSslCertificate::PatternSyntax::FixedString);
    if (localCerts.isEmpty())
        qCWarning(lcCoapClient, "The specified local certificate file is not valid.");
    else
        configuration.setLocalCertificateChain(localCerts.toVector());

    const auto caCerts = QSslCertificate::fromPath(QUrl(caCertificatePath).toLocalFile(), QSsl::Pem,
                                                   QSslCertificate::PatternSyntax::FixedString);
    if (caCerts.isEmpty())
        qCWarning(lcCoapClient, "The specified CA certificate file is not valid.");
    else
        configuration.setCaCertificates(caCerts.toVector());

    QFile privateKey(QUrl(privateKeyPath).toLocalFile());
    if (privateKey.open(QIODevice::ReadOnly)) {
        QCoapPrivateKey key(privateKey.readAll(), QSsl::Ec);
        configuration.setPrivateKey(key);
    } else {
        qCWarning(lcCoapClient) << "Unable to read the specified private key file"
                                << privateKeyPath;
    }
    m_configuration = configuration;
}

设置安全配置后,sendGetRequest 方法则设置请求 URL 并发送 GET 请求

void QmlCoapSecureClient::sendGetRequest(const QString &host, const QString &path, int port)
{
    if (!m_coapClient)
        return;

    m_coapClient->setSecurityConfiguration(m_configuration);

    QUrl url;
    url.setHost(host);
    url.setPath(path);
    url.setPort(port);
    m_coapClient->get(url);
}

在发送首次请求时,会与 CoAP 服务器进行握手。握手成功完成后,所有后续消息都会进行加密。成功握手后更改安全配置不会产生任何效果。如果需要更改,或更改主机,您需要先断开连接。

void QmlCoapSecureClient::disconnect()
{
    if (m_coapClient)
        m_coapClient->disconnect();
}

这将终止握手并关闭打开的套接字。

对于使用 X.509 证书的认证,需要指定证书文件。《code translate="no">FilePicker 组件用于此目的。它结合了一个文本框和一个按钮,当按钮被按下时,用于打开文件对话框

Item {
    id: filePicker

    property string dialogText
    property alias selectedFile: filePathField.text

    height: addFileButton.height

    FileDialog {
        id: fileDialog
        title: qsTr("Please Choose %1").arg(filePicker.dialogText)
        currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation)
        fileMode: FileDialog.OpenFile
        onAccepted: filePathField.text = fileDialog.selectedFile
    }

    RowLayout {
        anchors.fill: parent
        TextField {
            id: filePathField
            placeholderText: qsTr("<%1>").arg(filePicker.dialogText)
            inputMethodHints: Qt.ImhUrlCharactersOnly
            selectByMouse: true
            Layout.fillWidth: true
        }

        Button {
            id: addFileButton
            text: qsTr("Add %1").arg(filePicker.dialogText)
            onClicked: fileDialog.open()
        }
    }
}

FilePickerMain.qml 文件中多次实例化,用于创建证书和私钥的输入字段

FilePicker {
    id: localCertificatePicker
    dialogText: qsTr("Local Certificate")
    enabled: (securityModeGroup.checkedButton as RadioButton) === certificateMode
    Layout.columnSpan: 2
    Layout.fillWidth: true
}

FilePicker {
    id: caCertificatePicker
    dialogText: qsTr("CA Certificate")
    enabled: (securityModeGroup.checkedButton as RadioButton) === certificateMode
    Layout.columnSpan: 2
    Layout.fillWidth: true
}

FilePicker {
    id: privateKeyPicker
    dialogText: qsTr("Private Key")
    enabled: (securityModeGroup.checkedButton as RadioButton) === certificateMode
    Layout.columnSpan: 2
    Layout.fillWidth: true
}

设置安全的 CoAP 服务器

要运行此示例,您需要一个支持 PSK 或证书模式(或两者)的安全 CoAP 服务器。您有以下选项

  • 使用 libcoapCaliforniumFreeCoAP 或任何其他支持 DTLS 的 CoAP 库手动构建并运行一个安全的 CoAP 服务器
  • 使用 Docker Hub 上可用的现成 Docker 镜像来构建和运行适合我们示例的安全 CoAP 服务器。以下步骤描述了如何使用基于 Docker 的 CoAP 服务器
为 PSK 模式设置服务器

以下命令从 Docker Hub 拉取基于 Californium plugtest(默认为非安全模式)的 secure CoAP 服务器 Docker 容器并启动它

docker run --name coap-test-server -d --rm -p 5683:5683/udp -p 5684:5684/udp tqtc/coap-californium-test-server:3.8.0

CoAP 测试服务器将可通过端口 5683(非安全)和 5684(安全)访问。有关检索 IP 地址的说明,请参阅 获取 IP 地址

使用此服务器运行示例时,您需要将预共享密钥设置为 secretPSK,并将标识设置为 Client_identity

为证书模式设置服务器

使用 X.509 证书进行认证的 secure 服务器 Docker 镜像是基于 FreeCoAP 库中的 time server 示例。以下命令从 Docker Hub 拉取容器并启动它

docker run --name coap-time-server -d --rm -p 5684:5684/udp tqtc/coap-secure-time-server:freecoap

有关获取IP地址的说明,请参阅获取IP地址。CoAP测试服务器可通过获取的IP地址在端口5684和资源路径/time上访问。

要使用此服务器运行示例,您需要指定服务器所需的证书文件。它们位于docker容器中的/root/certs目录下。要将它们复制到本地目录,请使用以下命令

docker cp <container_id>:/root/certs <local_directory_path>

例如:

$ docker cp 5e46502df88f:/root/certs ~/

获取容器ID的说明如下。

获取IP地址

要查找docker容器的IP地址,首先通过运行docker ps命令获取容器ID,将输出类似的信息

$ docker ps
CONTAINER ID        IMAGE
5e46502df88f        tqtc/coap-californium-test-server:3.8.0

然后,您可以使用以下命令获取IP地址

docker inspect <container_id> | grep IPAddress

例如:

$ docker inspect 5e46502df88f | grep IPAddress
...
"IPAddress": "172.17.0.2",
...
终止Docker容器

要终止使用后的docker容器,请使用以下命令

docker stop <container_id>

这里的<container_id>与通过docker ps命令获取的相同。

文件

© 2024 Qt公司有限公司。此处包含的文档贡献是各自所有者的版权。此处提供的文档是根据自由软件基金会发布并许可的GNU自由文档许可协议版本1.3的条款许可的。Qt和相应的标志是芬兰以及/或其他国家的Qt公司有限公司的商标。所有其他商标均为各自所有者所有。