/** * @file OpcUaClient.cpp * @brief OpcUaClient implementation. */ #include "OpcUaClient.h" #include "OpcUaAuth.h" #include #include #include #include #include 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 (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 &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 (errorState->connectionStep ())) .arg (static_cast (errorState->errorCode ()), 8, 16, QLatin1Char ('0'))); } } void OpcUaClient::handleClientError (QOpcUaClient::ClientError error) { if (error == QOpcUaClient::NoError) return; auto me = QMetaEnum::fromType (); const char *key = me.valueToKey (static_cast (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 & 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 &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)")); } }