summaryrefslogtreecommitdiffstats
path: root/src/OpcUaClient.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/OpcUaClient.cpp')
-rw-r--r--src/OpcUaClient.cpp569
1 files changed, 569 insertions, 0 deletions
diff --git a/src/OpcUaClient.cpp b/src/OpcUaClient.cpp
new file mode 100644
index 0000000..91ce47b
--- /dev/null
+++ b/src/OpcUaClient.cpp
@@ -0,0 +1,569 @@
+/**
+ * @file OpcUaClient.cpp
+ * @brief OpcUaClient implementation.
+ */
+#include "OpcUaClient.h"
+#include "OpcUaAuth.h"
+
+#include <QDir>
+#include <QMetaEnum>
+#include <QOpcUaPkiConfiguration>
+#include <QOpcUaUserTokenPolicy>
+#include <QStandardPaths>
+
+using namespace Qt::Literals::StringLiterals;
+
+static constexpr qint32 DISCOVERY_INTERVAL = 30000; // ms
+
+static QString
+defaultPkiDir ()
+{
+ return QStandardPaths::writableLocation (QStandardPaths::AppDataLocation)
+ + QStringLiteral ("/pki");
+}
+
+/** @brief Create the standard OPC UA PKI directory tree. */
+static void
+ensurePkiDirs (const QString &base)
+{
+ for (const auto sub :
+ { u"own/certs"_s, u"own/private"_s, u"trusted/certs"_s,
+ u"trusted/crl"_s, u"issuers/certs"_s, u"issuers/crl"_s })
+ {
+ QDir ().mkpath (base + QLatin1Char ('/') + sub);
+ }
+}
+
+static QString
+securityPolicyUri (OpcUaClient::SecurityPolicy policy)
+{
+ switch (policy)
+ {
+ case OpcUaClient::Basic256Sha256:
+ return QStringLiteral (
+ "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
+ case OpcUaClient::Aes128_Sha256_RsaOaep:
+ return QStringLiteral (
+ "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep");
+ case OpcUaClient::Aes256_Sha256_RsaPss:
+ return QStringLiteral (
+ "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss");
+ }
+ return {};
+}
+
+/* ======================================
+ * Construction
+ * ====================================== */
+
+OpcUaClient::OpcUaClient (QObject *parent)
+ : QObject (parent), m_provider (new QOpcUaProvider (this)),
+ m_pkiDir (defaultPkiDir ())
+{
+ Q_ASSERT (!s_instance);
+ ensurePkiDirs (m_pkiDir);
+ setupClient ();
+ autoDetectPki ();
+ applyPki ();
+ connect (&m_discoveryTimer, &QTimer::timeout, this,
+ &OpcUaClient::doDiscovery);
+ s_instance = this;
+}
+
+void
+OpcUaClient::setupClient ()
+{
+ m_client = m_provider->createClient (QStringLiteral ("open62541"));
+ if (!m_client)
+ {
+ qWarning () << "OpcUaClient: failed to create open62541 backend";
+ return;
+ }
+
+ connect (m_client, &QOpcUaClient::stateChanged, this,
+ &OpcUaClient::handleStateChanged);
+ connect (m_client, &QOpcUaClient::endpointsRequestFinished, this,
+ &OpcUaClient::handleEndpointsReceived);
+ connect (m_client, &QOpcUaClient::connectError, this,
+ &OpcUaClient::handleConnectError);
+ connect (m_client, &QOpcUaClient::findServersFinished, this,
+ &OpcUaClient::handleFindServersFinished);
+ connect (m_client, &QOpcUaClient::errorChanged, this,
+ &OpcUaClient::handleClientError);
+}
+
+/* ======================================
+ * Connection
+ * ====================================== */
+
+OpcUaClient *
+OpcUaClient::instance ()
+{
+ return s_instance;
+}
+
+bool
+OpcUaClient::connected () const
+{
+ return m_connected;
+}
+
+QString
+OpcUaClient::serverUrl () const
+{
+ return m_serverUrl;
+}
+
+void
+OpcUaClient::setServerUrl (const QString &url)
+{
+ if (m_serverUrl == url)
+ return;
+ m_serverUrl = url;
+ emit serverUrlChanged ();
+}
+
+OpcUaAuth *
+OpcUaClient::auth () const
+{
+ return m_auth;
+}
+
+void
+OpcUaClient::setAuth (OpcUaAuth *auth)
+{
+ if (m_auth == auth)
+ return;
+ m_auth = auth;
+ emit authChanged ();
+}
+
+QOpcUaClient *
+OpcUaClient::opcuaClient () const
+{
+ return m_client;
+}
+
+void
+OpcUaClient::connectToServer ()
+{
+ 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;
+ }
+
+ QUrl url (m_serverUrl);
+ if (!url.isValid ())
+ {
+ emit connectionError (
+ QStringLiteral ("Invalid server URL: %1").arg (m_serverUrl));
+ return;
+ }
+ m_client->requestEndpoints (url);
+}
+
+void
+OpcUaClient::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 OpcUaAuth::Anonymous:
+ tokenPolicy.setTokenType (
+ QOpcUaUserTokenPolicy::TokenType::Anonymous);
+ break;
+ case OpcUaAuth::UserPass:
+ tokenPolicy.setTokenType (
+ QOpcUaUserTokenPolicy::TokenType::Username);
+ break;
+ case OpcUaAuth::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
+OpcUaClient::disconnectFromServer ()
+{
+ if (m_client)
+ m_client->disconnectFromEndpoint ();
+}
+
+void
+OpcUaClient::acceptCertificate ()
+{
+ m_certAccepted = true;
+ if (m_certLoop)
+ m_certLoop->quit ();
+}
+
+void
+OpcUaClient::rejectCertificate ()
+{
+ m_certAccepted = false;
+ if (m_certLoop)
+ m_certLoop->quit ();
+}
+
+void
+OpcUaClient::handleStateChanged (QOpcUaClient::ClientState state)
+{
+ bool nowConnected = (state == QOpcUaClient::Connected);
+ if (m_connected != nowConnected)
+ {
+ m_connected = nowConnected;
+ emit connectedChanged ();
+ }
+}
+
+void
+OpcUaClient::handleEndpointsReceived (
+ const QList<QOpcUaEndpointDescription> &endpoints,
+ QOpcUa::UaStatusCode statusCode, const QUrl &)
+{
+ if (statusCode != QOpcUa::Good || endpoints.isEmpty ())
+ {
+ emit connectionError (QStringLiteral ("Failed to retrieve endpoints"));
+ return;
+ }
+
+ // Pick the endpoint with the highest securityLevel (server-assigned score).
+ QOpcUaEndpointDescription best = endpoints.first ();
+ for (const auto &ep : endpoints)
+ {
+ if (ep.securityLevel () > best.securityLevel ())
+ best = ep;
+ }
+
+ if (m_auth)
+ m_client->setAuthenticationInformation (
+ m_auth->toAuthenticationInformation ());
+
+ m_client->connectToEndpoint (best);
+}
+
+void
+OpcUaClient::handleConnectError (QOpcUaErrorState *errorState)
+{
+ if (errorState->connectionStep ()
+ == QOpcUaErrorState::ConnectionStep::CertificateValidation)
+ {
+ // connectError uses BlockingQueuedConnection — the backend thread is
+ // blocked waiting for us to return. The errorState pointer is stack-
+ // allocated in the backend, so it is only valid during this call.
+ // Spin a local event loop so QML can show a dialog and call
+ // acceptCertificate() / rejectCertificate() while we stay in scope.
+ m_certAccepted = false;
+ emit certificateTrustRequested (
+ QStringLiteral ("The server certificate is not trusted. Accept?"));
+
+ QEventLoop loop;
+ m_certLoop = &loop;
+ QTimer::singleShot (30000, &loop, &QEventLoop::quit);
+ loop.exec ();
+ m_certLoop = nullptr;
+
+ errorState->setIgnoreError (m_certAccepted);
+ }
+ else
+ {
+ emit connectionError (
+ QStringLiteral ("Connection error at step %1, code 0x%2")
+ .arg (static_cast<int> (errorState->connectionStep ()))
+ .arg (static_cast<quint32> (errorState->errorCode ()), 8, 16,
+ QLatin1Char ('0')));
+ }
+}
+
+void
+OpcUaClient::handleClientError (QOpcUaClient::ClientError error)
+{
+ if (error == QOpcUaClient::NoError)
+ return;
+ auto me = QMetaEnum::fromType<QOpcUaClient::ClientError> ();
+ const char *key = me.valueToKey (static_cast<int> (error));
+ emit connectionError (
+ QStringLiteral ("Client error: %1")
+ .arg (key ? QLatin1StringView (key) : "Unknown"_L1));
+}
+
+/* ======================================
+ * Discovery
+ * ====================================== */
+
+QString
+OpcUaClient::discoveryUrl () const
+{
+ return m_discoveryUrl;
+}
+
+void
+OpcUaClient::setDiscoveryUrl (const QString &url)
+{
+ if (m_discoveryUrl == url)
+ return;
+ m_discoveryUrl = url;
+ emit discoveryUrlChanged ();
+}
+
+bool
+OpcUaClient::discovering () const
+{
+ return m_discovering;
+}
+
+const QList<QOpcUaApplicationDescription> &
+OpcUaClient::discoveredServers () const
+{
+ return m_discoveredServers;
+}
+
+QVariantList
+OpcUaClient::servers () const
+{
+ return m_serversCache;
+}
+
+void
+OpcUaClient::startDiscovery ()
+{
+ if (m_discoveryUrl.isEmpty () || !m_client)
+ return;
+
+ doDiscovery ();
+ m_discoveryTimer.start (DISCOVERY_INTERVAL);
+
+ if (!m_discovering)
+ {
+ m_discovering = true;
+ emit discoveringChanged ();
+ }
+}
+
+void
+OpcUaClient::stopDiscovery ()
+{
+ m_discoveryTimer.stop ();
+
+ if (m_discovering)
+ {
+ m_discovering = false;
+ emit discoveringChanged ();
+ }
+}
+
+void
+OpcUaClient::doDiscovery ()
+{
+ if (!m_client || m_discoveryUrl.isEmpty ())
+ return;
+ QUrl url (m_discoveryUrl);
+ if (!url.isValid ())
+ return;
+ m_client->findServers (url);
+}
+
+void
+OpcUaClient::handleFindServersFinished (
+ const QList<QOpcUaApplicationDescription> &servers,
+ QOpcUa::UaStatusCode statusCode, const QUrl &)
+{
+ if (statusCode != QOpcUa::Good)
+ return;
+
+ m_discoveredServers = servers;
+ m_serversCache.clear ();
+ for (const auto &s : m_discoveredServers)
+ {
+ QVariantMap entry;
+ entry[QStringLiteral ("serverName")] = s.applicationName ().text ();
+ entry[QStringLiteral ("applicationUri")] = s.applicationUri ();
+ entry[QStringLiteral ("discoveryUrls")]
+ = QVariant::fromValue (s.discoveryUrls ());
+ m_serversCache.append (entry);
+ }
+ emit serversChanged ();
+}
+
+/* ======================================
+ * PKI
+ * ====================================== */
+
+QString
+OpcUaClient::pkiDir () const
+{
+ return m_pkiDir;
+}
+
+void
+OpcUaClient::setPkiDir (const QString &path)
+{
+ if (m_pkiDir == path)
+ return;
+ m_pkiDir = path;
+ ensurePkiDirs (m_pkiDir);
+ emit pkiDirChanged ();
+}
+
+QString
+OpcUaClient::certFile () const
+{
+ return m_certFile;
+}
+
+void
+OpcUaClient::setCertFile (const QString &path)
+{
+ if (m_certFile == path)
+ return;
+ m_certFile = path;
+ emit certFileChanged ();
+}
+
+QString
+OpcUaClient::keyFile () const
+{
+ return m_keyFile;
+}
+
+void
+OpcUaClient::setKeyFile (const QString &path)
+{
+ if (m_keyFile == path)
+ return;
+ m_keyFile = path;
+ emit keyFileChanged ();
+}
+
+void
+OpcUaClient::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
+OpcUaClient::applyPki ()
+{
+ if (!m_client || m_pkiDir.isEmpty ())
+ return;
+
+ if (!m_certFile.isEmpty () && !QFile::exists (m_certFile))
+ {
+ emit statusMessage (
+ QStringLiteral ("PKI error: certificate not found: %1")
+ .arg (m_certFile));
+ return;
+ }
+ if (!m_keyFile.isEmpty () && !QFile::exists (m_keyFile))
+ {
+ emit statusMessage (
+ QStringLiteral ("PKI error: private key not found: %1")
+ .arg (m_keyFile));
+ return;
+ }
+ if (!m_certFile.isEmpty () && m_keyFile.isEmpty ())
+ {
+ emit statusMessage (
+ QStringLiteral ("PKI error: certificate set but no private key"));
+ return;
+ }
+ if (m_certFile.isEmpty () && !m_keyFile.isEmpty ())
+ {
+ emit statusMessage (
+ QStringLiteral ("PKI error: private key set but no certificate"));
+ return;
+ }
+
+ QOpcUaPkiConfiguration pki;
+ if (!m_certFile.isEmpty ())
+ pki.setClientCertificateFile (m_certFile);
+ if (!m_keyFile.isEmpty ())
+ pki.setPrivateKeyFile (m_keyFile);
+ pki.setTrustListDirectory (m_pkiDir + QStringLiteral ("/trusted/certs"));
+ pki.setRevocationListDirectory (m_pkiDir + QStringLiteral ("/trusted/crl"));
+ pki.setIssuerListDirectory (m_pkiDir + QStringLiteral ("/issuers/certs"));
+ pki.setIssuerRevocationListDirectory (m_pkiDir
+ + QStringLiteral ("/issuers/crl"));
+
+ m_client->setPkiConfiguration (pki);
+
+ if (pki.isKeyAndCertificateFileSet ())
+ {
+ auto identity = pki.applicationIdentity ();
+ if (!identity.isValid ())
+ {
+ emit statusMessage (QStringLiteral (
+ "PKI error: certificate could not be parsed (invalid DER?)"));
+ return;
+ }
+ m_client->setApplicationIdentity (identity);
+ emit statusMessage (QStringLiteral ("PKI applied: %1")
+ .arg (m_certFile.split ('/').last ()));
+ }
+ else
+ {
+ emit statusMessage (
+ QStringLiteral ("PKI applied (no client certificate)"));
+ }
+}