summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-18 12:03:16 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-18 12:03:16 +0100
commit23916cbb98e952aab752a647ac96020aab709bb6 (patch)
tree81fc9b5e551300ab348b48fa6bffbc5fddf2c95a
parent1a79ab468d8cc23cfdf28ddfa85d3e03ffddf44c (diff)
downloadBobinkQtOpcUa-23916cbb98e952aab752a647ac96020aab709bb6.tar.gz
BobinkQtOpcUa-23916cbb98e952aab752a647ac96020aab709bb6.zip
Add direct connect, auto-detect PKI, and rework demo flow
- connectDirect(policy, mode): connect without endpoint discovery, for servers with no unencrypted endpoint. Sets user identity token policy matching the auth mode. - autoDetectPki(): scan own/certs/*.der and own/private/*.pem|crt, called automatically at startup. - Demo: discovery auto-starts, PKI section hidden behind toggle with auto-detected cert summary, direct connect appears on connect failure. File/folder dialogs for manual PKI override.
-rw-r--r--demo/Main.qml145
-rw-r--r--src/BobinkClient.cpp79
-rw-r--r--src/BobinkClient.h20
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 ();