diff options
| -rw-r--r-- | demo/Main.qml | 145 | ||||
| -rw-r--r-- | src/BobinkClient.cpp | 79 | ||||
| -rw-r--r-- | src/BobinkClient.h | 20 |
3 files changed, 230 insertions, 14 deletions
diff --git a/demo/Main.qml b/demo/Main.qml index 5609c12..fe4f369 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -3,14 +3,23 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Dialogs import Bobink ApplicationWindow { width: 600 - height: 600 + height: 800 visible: true title: "Bobink Demo" + property bool autoConnectFailed: false + property bool showPkiSettings: false + + Component.onCompleted: { + BobinkClient.discoveryUrl = discoveryUrlField.text + BobinkClient.startDiscovery() + } + Connections { target: BobinkClient function onServersChanged() { @@ -18,9 +27,12 @@ ApplicationWindow { } function onConnectedChanged() { console.log("Connected:", BobinkClient.connected) + if (BobinkClient.connected) + autoConnectFailed = false } function onConnectionError(message) { console.log("Connection error:", message) + autoConnectFailed = true } function onDiscoveringChanged() { console.log("Discovering:", BobinkClient.discovering) @@ -92,42 +104,96 @@ ApplicationWindow { ScrollBar.vertical: ScrollBar {} } - Label { text: "PKI"; font.bold: true } + RowLayout { + Label { text: "PKI"; font.bold: true } + Label { + text: BobinkClient.certFile + ? " (" + BobinkClient.certFile.split("/").pop() + ")" + : " (no certificate found)" + font.italic: true + color: BobinkClient.certFile ? "green" : "gray" + } + Item { Layout.fillWidth: true } + Button { + text: showPkiSettings ? "Hide" : "Configure..." + flat: true + onClicked: showPkiSettings = !showPkiSettings + } + } + + FileDialog { + id: certFileDialog + title: "Select Certificate" + currentFolder: "file://" + trustFolderField.text + nameFilters: ["DER certificates (*.der)", "All files (*)"] + onAccepted: certFileField.text = selectedFile.toString().replace("file://", "") + } + + FileDialog { + id: keyFileDialog + title: "Select Private Key" + currentFolder: "file://" + trustFolderField.text + nameFilters: ["Key files (*.pem *.crt)", "All files (*)"] + onAccepted: keyFileField.text = selectedFile.toString().replace("file://", "") + } + + FolderDialog { + id: trustFolderDialog + title: "Select Trust Folder" + currentFolder: "file://" + trustFolderField.text + onAccepted: trustFolderField.text = selectedFolder.toString().replace("file://", "") + } GridLayout { - columns: 2 + columns: 3 Layout.fillWidth: true + visible: showPkiSettings - Label { text: "PKI directory:" } - TextField { - id: pkiDirField - Layout.fillWidth: true - text: BobinkClient.pkiDir - onEditingFinished: BobinkClient.pkiDir = text - } Label { text: "Certificate:" } TextField { id: certFileField Layout.fillWidth: true - placeholderText: "/path/to/cert.der" + text: BobinkClient.certFile + placeholderText: "Client certificate (.der)" } + Button { + text: "Browse..." + onClicked: certFileDialog.open() + } + Label { text: "Private key:" } TextField { id: keyFileField Layout.fillWidth: true - placeholderText: "/path/to/key.pem" + text: BobinkClient.keyFile + placeholderText: "Private key (.pem, .crt)" + } + Button { + text: "Browse..." + onClicked: keyFileDialog.open() + } + + Label { text: "Trust folder:" } + TextField { + id: trustFolderField + Layout.fillWidth: true + text: BobinkClient.pkiDir + } + Button { + text: "Browse..." + onClicked: trustFolderDialog.open() } } Button { text: "Apply PKI" Layout.fillWidth: true + visible: showPkiSettings onClicked: { - BobinkClient.pkiDir = pkiDirField.text + BobinkClient.pkiDir = trustFolderField.text BobinkClient.certFile = certFileField.text BobinkClient.keyFile = keyFileField.text BobinkClient.applyPki() - console.log("PKI applied:", BobinkClient.pkiDir) } } @@ -206,6 +272,7 @@ ApplicationWindow { if (BobinkClient.connected) { BobinkClient.disconnectFromServer() } else { + autoConnectFailed = false BobinkClient.auth = auth BobinkClient.serverUrl = serverUrlField.text BobinkClient.connectToServer() @@ -214,6 +281,56 @@ ApplicationWindow { } Label { + text: "Direct Connect" + font.bold: true + visible: autoConnectFailed && !BobinkClient.connected + } + + GridLayout { + columns: 2 + Layout.fillWidth: true + visible: autoConnectFailed && !BobinkClient.connected + + Label { text: "Security policy:" } + ComboBox { + id: securityPolicyCombo + Layout.fillWidth: true + textRole: "text" + valueRole: "policy" + model: [ + { text: "Basic256Sha256", policy: BobinkClient.Basic256Sha256 }, + { text: "Aes128-Sha256-RsaOaep", policy: BobinkClient.Aes128_Sha256_RsaOaep }, + { text: "Aes256-Sha256-RsaPss", policy: BobinkClient.Aes256_Sha256_RsaPss } + ] + } + + Label { text: "Security mode:" } + ComboBox { + id: securityModeCombo + Layout.fillWidth: true + textRole: "text" + valueRole: "mode" + model: [ + { text: "Sign & Encrypt", mode: BobinkClient.SignAndEncrypt }, + { text: "Sign", mode: BobinkClient.Sign }, + { text: "None", mode: BobinkClient.None } + ] + } + } + + Button { + text: "Direct Connect" + Layout.fillWidth: true + visible: autoConnectFailed && !BobinkClient.connected + onClicked: { + BobinkClient.auth = auth + BobinkClient.serverUrl = serverUrlField.text + BobinkClient.connectDirect(securityPolicyCombo.currentValue, + securityModeCombo.currentValue) + } + } + + Label { text: "Status: " + (BobinkClient.connected ? "Connected" : "Disconnected") color: BobinkClient.connected ? "green" : "red" } diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp index a067489..565868c 100644 --- a/src/BobinkClient.cpp +++ b/src/BobinkClient.cpp @@ -6,6 +6,7 @@ #include "BobinkAuth.h" #include <QDir> +#include <QOpcUaUserTokenPolicy> #include <QStandardPaths> BobinkClient *BobinkClient::s_instance = nullptr; @@ -33,6 +34,8 @@ BobinkClient::BobinkClient(QObject *parent) { ensurePkiDirs(m_pkiDir); setupClient(); + autoDetectPki(); + applyPki(); connect(&m_discoveryTimer, &QTimer::timeout, this, &BobinkClient::doDiscovery); } @@ -129,6 +132,65 @@ void BobinkClient::connectToServer() m_client->requestEndpoints(url); } +static QString securityPolicyUri(BobinkClient::SecurityPolicy policy) +{ + switch (policy) { + case BobinkClient::Basic256Sha256: + return QStringLiteral( + "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); + case BobinkClient::Aes128_Sha256_RsaOaep: + return QStringLiteral( + "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"); + case BobinkClient::Aes256_Sha256_RsaPss: + return QStringLiteral( + "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss"); + } + return {}; +} + +void BobinkClient::connectDirect(SecurityPolicy policy, SecurityMode mode) +{ + if (!m_client) { + emit connectionError(QStringLiteral("OPC UA backend not available")); + return; + } + if (m_serverUrl.isEmpty()) { + emit connectionError(QStringLiteral("No server URL set")); + return; + } + if (m_client->state() != QOpcUaClient::Disconnected) { + emit connectionError(QStringLiteral("Already connected or connecting")); + return; + } + + QOpcUaEndpointDescription endpoint; + endpoint.setEndpointUrl(m_serverUrl); + endpoint.setSecurityPolicy(securityPolicyUri(policy)); + endpoint.setSecurityMode( + static_cast<QOpcUaEndpointDescription::MessageSecurityMode>(mode)); + + QOpcUaUserTokenPolicy tokenPolicy; + if (m_auth) { + switch (m_auth->mode()) { + case BobinkAuth::Anonymous: + tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Anonymous); + break; + case BobinkAuth::UserPass: + tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Username); + break; + case BobinkAuth::Certificate: + tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Certificate); + break; + } + m_client->setAuthenticationInformation(m_auth->toAuthenticationInformation()); + } else { + tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Anonymous); + } + endpoint.setUserIdentityTokens({tokenPolicy}); + + m_client->connectToEndpoint(endpoint); +} + void BobinkClient::disconnectFromServer() { if (m_client) @@ -223,6 +285,23 @@ void BobinkClient::setKeyFile(const QString &path) emit keyFileChanged(); } +void BobinkClient::autoDetectPki() +{ + if (m_pkiDir.isEmpty()) + return; + + QDir certDir(m_pkiDir + QStringLiteral("/own/certs")); + QStringList certs = certDir.entryList({QStringLiteral("*.der")}, QDir::Files); + if (!certs.isEmpty()) + setCertFile(certDir.filePath(certs.first())); + + QDir keyDir(m_pkiDir + QStringLiteral("/own/private")); + QStringList keys = keyDir.entryList( + {QStringLiteral("*.pem"), QStringLiteral("*.crt")}, QDir::Files); + if (!keys.isEmpty()) + setKeyFile(keyDir.filePath(keys.first())); +} + void BobinkClient::applyPki() { if (!m_client || m_pkiDir.isEmpty()) diff --git a/src/BobinkClient.h b/src/BobinkClient.h index f95ab02..43eda65 100644 --- a/src/BobinkClient.h +++ b/src/BobinkClient.h @@ -88,8 +88,26 @@ public: QString keyFile () const; void setKeyFile (const QString &path); + enum SecurityMode + { + SignAndEncrypt = 3, + Sign = 2, + None = 1, + }; + Q_ENUM (SecurityMode) + + enum SecurityPolicy + { + Basic256Sha256, + Aes128_Sha256_RsaOaep, + Aes256_Sha256_RsaPss, + }; + Q_ENUM (SecurityPolicy) + /** @brief Discover endpoints, pick the most secure, connect. */ Q_INVOKABLE void connectToServer (); + /** @brief Connect directly without endpoint discovery. */ + Q_INVOKABLE void connectDirect (SecurityPolicy policy, SecurityMode mode); Q_INVOKABLE void disconnectFromServer (); /** @brief Accept the pending server certificate. */ @@ -100,6 +118,8 @@ public: Q_INVOKABLE void startDiscovery (); Q_INVOKABLE void stopDiscovery (); + /** @brief Auto-detect cert/key from the PKI directory and apply. */ + Q_INVOKABLE void autoDetectPki (); /** @brief Apply PKI dirs and cert/key. Call before connecting. */ Q_INVOKABLE void applyPki (); |
