Source code
Revision control
Copy as Markdown
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qopcuagdsclient_p.h"
#include <QOpcUaProvider>
#include <QOpcUaExtensionObject>
#include <QOpcUaBinaryDataEncoding>
#include <QOpcUaErrorState>
#include <QOpcUaKeyPair>
#include <QOpcUaX509ExtensionSubjectAlternativeName>
#include <QOpcUaX509ExtensionBasicConstraints>
#include <QOpcUaX509ExtensionKeyUsage>
#include <QFile>
#include <QTimer>
#include <QTemporaryFile>
#include <QSettings>
#include <QSslCertificate>
#include <QtCore/qloggingcategory.h>
QT_BEGIN_NAMESPACE
using namespace Qt::Literals::StringLiterals;
Q_STATIC_LOGGING_CATEGORY(QT_OPCUA_GDSCLIENT, "qt.opcua.gdsclient")
static const QStringList elementsToResolve {
u"GetApplication"_s,
u"FindApplications"_s,
u"RegisterApplication"_s,
u"UnregisterApplication"_s,
u"GetCertificateGroups"_s,
u"GetCertificateStatus"_s,
u"StartSigningRequest"_s,
u"FinishRequest"_s,
u"GetTrustList"_s,
};
/*!
\class QOpcUaGdsClient
\inmodule QtOpcUa
\since 5.14
\brief Handles communication with the GDS Server.
This class is currently available as a Technology Preview, and therefore the API
and functionality provided by the class may be subject to change at any time without
prior notice.
This class handles all steps needed for communication with a GDS server. Provided with
information about the application it does registering with the server and managing
key/certificates.
Only few details need to be known in order to take part in a secured network.
First time registration requires administrative privileges using username and password
for authentication. All further authentications are application based, using the certificate
which was received first.
Expecting the whole process to succeed, you have to wait for the applicationRegistered signal.
Most of the setup structs have to be the same as for the connection with QOpcUaClient afterwards
and can be shared.
Setting up a GDS client:
\code
QOpcUaGdsClient c;
// In case the credentials are needed
QObject::connect(&c, &QOpcUaGdsClient::authenticationRequired, [&](QOpcUaAuthenticationInformation &authInfo) {
authInfo.setUsernameAuthentication("root", "secret");
});
// Await success
QObject::connect(&c, &QOpcUaGdsClient::applicationRegistered, [&]() {
qDebug() << "Application" << c.applicationId() << "registered";
});
c.setBackend(...);
c.setEndpoint(...);
c.setApplicationIdentity(...);
c.setPkiConfiguration(...);
c.setApplicationRecord(...);
c.setCertificateSigningRequestPresets(...);
c.start();
\endcode
*/
/*!
\enum QOpcUaGdsClient::Error
This enum is used to specify errors, that could happen during the registration
process.
\value NoError
Everying is fine
\value InvalidBackend
The backend could not be instantiated.
The backend string given, does not match any backend or loading the plugin failed.
\value InvalidEndpoint
The given endpoint is not valid.
\value ConnectionError
The connection to the server endpoint failed.
\value DirectoryNodeNotFound
The directory node on the server could not be resolved
\value FailedToRegisterApplication
The registration of the application was not successful.
\value FailedToUnregisterApplication
The unregistration of the application was not successful.
\value FailedToGetCertificateStatus
The status of the current certificate could not be retrieved.
\value FailedToGetCertificate
A certificate could not be retrieved from the server.
*/
/*!
\enum QOpcUaGdsClient::State
This enum is used to specify the current state of the registration of the GDS
client.
\value Idle
The client was not started yet.
\value BackendInstantiated
The backend was instantiated
\value Connecting
A connecting to the server is being made
\value Connected
The connection to the server endpoint was successful.
\value RegisteringApplication
The application is being registered with the server.
\value ApplicationRegistered
Registering the application with the server was successful.
\value Error
An error happened. See the return value of error() and the terminal output
for more details.
*/
/*!
\fn QOpcUaGdsClient::stateChanged(State state)
This signal is emitted when the internal state of the client changes.
The \a state indicates the new state.
*/
/*!
\fn QOpcUaGdsClient::errorChanged(Error error)
This signal is emitted when an \a error occurred.
*/
/*!
\fn QOpcUaGdsClient::applicationRegistered()
This signal is emitted when an application was registered successfully.
*/
/*!
\fn QOpcUaGdsClient::certificateGroupsReceived(QStringList certificateGroups)
This signal is emitted when the GDS client receives a new list of \a certificateGroups
for this application.
*/
/*!
\fn QOpcUaGdsClient::certificateUpdateRequired()
This signal is emitted when the GDS client detects that an update of the currently
used certificate is necessary.
This could be caused by the server, requesting the client to update the certificate,
when the certificate's due date is met or if the certificate is self-signed.
The certificate update is handled automatically. This signal is only for informational
purpose that an update is going to happen.
*/
/*!
\fn QOpcUaGdsClient::certificateUpdated()
This signal is emitted when the GDS client received a new certificate that was
stored on disk.
*/
/*!
\fn QOpcUaGdsClient::unregistered()
This signal is emitted when the GDS client has unregistered the application.
*/
/*!
\fn QOpcUaGdsClient::trustListUpdated()
This signal is emitted when the GDS client has received a new trust list from the
server and stored to disk.
*/
/*!
\fn QOpcUaGdsClient::authenticationRequired(QOpcUaAuthenticationInformation &authInfo)
This signal is emitted when the GDS client tries to do a first time authentication
with a server, that requires administrative privileges.
\a authInfo has to be filled with valid authentication information.
This slot must not be used crossing thread boundaries.
*/
/*
Step 1
Create a self-signed certificate for the application.
Previously created key and certificate are reused if present.
Step 2
Connect to the given endpoint and wait for the namespace array to be updated.
Step 3
Find the root node of the GDS API
Step 4
From the root node find the "Directory" node where the method nodes are located.
- resolveDirectoryNode()
- lambda
Step 5
Resolve the method nodes that are being used.
- resolveMethodNodes()
- _q_handleResolveBrowsePathFinished()
Step 6a
In case a previous appliation id is known, check if the application registration is still valid.
In case it is still valid, reuse it.
- getApplication()
- handleGetApplicationFinished()
Step 6b
In case there is no valid application id known, try to find a previous registration by the application URI.
- findRegisteredApplication()
- handleFindApplicationsFinished()
Step 6c
In case an application id was not found by the application URI, register the application.
- registerApplication()
- handleRegisterApplicationFinished()
Step 7
Get the certificate groups of the application
- getCertificateGroups()
- handleGetCertificateGroupsFinished()
- emit signal certificateGroupsReceived
Step 8
Resolve the certificate types from the certificate groups (CertificateGroup->CertificateTypes)
- resolveCertificateTypes()
- lambda
Step 9
Read certificate type value from the resolved nodes
- getCertificateTypes()
- lambda
Step 10
The application now is using a application id from 6a, 6b or 6c, resolved the certificates and is ready to interact with the GDS.
- registrationDone()
- emit signal applicationRegistered
-> Start a certificate check
-> Start a trust list update
Depending on the certificate state, the certificate needs to be updated by the client.
This may happen due to
- the server requires it (GetCertificateStatus)
- the expiry date is due
- the certificate is self-signed from the initial connection
Step 1
A timer will trigger the certificate check regularly by triggering
- _q_certificateCheckTimeout()
- emit signal certificateUpdateRequired
Step 2a
Check locally first, if a renew condition is met.
Otherwise ask the server.
Step 2b
Ask the server if the certificate needs renewal
- getCertificateStatus()
- handleGetCertificateStatusFinished()
- emit signal certificateUpdateRequired
Step 3
Start a certificate signing request
- startCertificateRequest()
- handleStartSigningRequestFinished()
Step 4
Regularly ask the server for the certificate signing result
- finishCertificateRequest()
- handleFinishRequestFinished()
- emit signal certificateUpdated
This will return a new certificate.
*/
/*!
Constructs a GDS client with \a parent as the parent object.
*/
QOpcUaGdsClient::QOpcUaGdsClient(QObject *parent)
: QObject(*(new QOpcUaGdsClientPrivate()), parent)
{
Q_D(QOpcUaGdsClient);
d->initializePrivateConnections();
}
/*!
Destructs a GDS client.
*/
QOpcUaGdsClient::~QOpcUaGdsClient()
{
}
/*!
Sets the backend to be used by the client to communicate with the server to \a backend.
This function has to be called before starting the GDS client.
Changing this setting afterwards has no effect.
\sa QOpcUaProvider::availableBackends() start()
*/
void QOpcUaGdsClient::setBackend(const QString &backend)
{
Q_D(QOpcUaGdsClient);
d->setBackend(backend);
}
/*!
Returns the current backend setting.
If the backend was changed after starting the client, it will return
the changed setting, but not the actually used instance.
*/
const QString &QOpcUaGdsClient::backend() const
{
Q_D(const QOpcUaGdsClient);
return d->backend();
}
/*!
Sets the endpoint to be used by the client to communicate with the server to \a endpoint.
This function has to be called before starting the GDS client.
Changing this setting afterwards has no effect.
Communication to a GDS server is only possible through an encrypted endpoint.
Using an unencrypted endpoint will fail.
*/
void QOpcUaGdsClient::setEndpoint(const QOpcUaEndpointDescription &endpoint)
{
Q_D(QOpcUaGdsClient);
d->setEndpoint(endpoint);
}
/*!
Returns the current endpoint setting.
If the endpoint was changed after starting the client, it will return
the changed setting, but not the actually used endpoint.
*/
const QOpcUaEndpointDescription &QOpcUaGdsClient::endpoint() const
{
Q_D(const QOpcUaGdsClient);
return d->endpoint();
}
/*!
Sets the PKI configuration \a pkiConfig to be used by the client.
All certificates, keys and trust lists will be used from or stored to
the locations given. In order to use the certificate received from
the GDS, the same configuration has to be used with QOpcUaClient.
This function has to be called before starting the GDS client.
Changing this setting afterwards has no effect.
*/
void QOpcUaGdsClient::setPkiConfiguration(const QOpcUaPkiConfiguration &pkiConfig)
{
Q_D(QOpcUaGdsClient);
d->setPkiConfiguration(pkiConfig);
}
/*!
Returns the current pkiConfiguration.
*/
const QOpcUaPkiConfiguration &QOpcUaGdsClient::pkiConfiguration() const
{
Q_D(const QOpcUaGdsClient);
return d->pkiConfiguration();
}
/*!
Sets the application identity \a appIdentity to be used by the client.
This identity is used to register with the GDS server.
This function has to be called before starting the GDS client.
Changing this setting afterwards has no effect.
*/
void QOpcUaGdsClient::setApplicationIdentity(const QOpcUaApplicationIdentity &appIdentity)
{
Q_D(QOpcUaGdsClient);
d->setApplicationIdentity(appIdentity);
}
/*!
Returns the current applicationIdentity.
*/
const QOpcUaApplicationIdentity &QOpcUaGdsClient::applicationIdentity() const
{
Q_D(const QOpcUaGdsClient);
return d->applicationIdentity();
}
/*!
Sets the application record data \a appRecord to be used by the client.
This data is used to register with the GDS server.
This function has to be called before starting the GDS client.
Most of the data is the same as in the application identity.
After registration the assigned application id can be retrieved.
\sa setApplicationIdentity
*/
void QOpcUaGdsClient::setApplicationRecord(const QOpcUaApplicationRecordDataType &appRecord)
{
Q_D(QOpcUaGdsClient);
d->setApplicationRecord(appRecord);
}
/*!
Returns the application record data that is used by the client.
*/
const QOpcUaApplicationRecordDataType &QOpcUaGdsClient::applicationRecord() const
{
Q_D(const QOpcUaGdsClient);
return d->applicationRecord();
}
/*!
Returns the application id assigned by the server.
Is is a shortcut to receive the application id from the application record data.
\sa applicationRecord()
*/
QString QOpcUaGdsClient::applicationId() const
{
Q_D(const QOpcUaGdsClient);
return d->applicationId();
}
/*!
Sets the presets for certificate siging requests; the distinguished name \a dn and
the DNS string \a dns.
When creating a certificate signing request some additional information is needed,
that is not provided by the application identity.
This function has to be called before starting the GDS client.
\sa setApplicationIdentity()
*/
void QOpcUaGdsClient::setCertificateSigningRequestPresets(const QOpcUaX509DistinguishedName &dn, const QString &dns)
{
Q_D(QOpcUaGdsClient);
d->setCertificateSigningRequestPresets(dn, dns);
}
/*!
Returns the distinguished name preset for certificate siging requests.
*/
const QOpcUaX509DistinguishedName &QOpcUaGdsClient::distinguishedNameCertificateSigningRequestPreset() const
{
Q_D(const QOpcUaGdsClient);
return d->distinguishedNameCertificateSigningRequestPreset();
}
/*!
Returns the DNS preset for certificate siging requests.
*/
const QString &QOpcUaGdsClient::dnsCertificateSigningRequestPreset() const
{
Q_D(const QOpcUaGdsClient);
return d->dnsCertificateSigningRequestPreset();
}
/*!
Sets the interval in milliseconds for checking the validity of the client certificate
to \a interval.
*/
void QOpcUaGdsClient::setCertificateCheckInterval(int interval)
{
Q_D(QOpcUaGdsClient);
d->setCertificateCheckInterval(interval);
}
/*!
Returns the interval in milliseconds for checking the validity of the client certificate.
*/
int QOpcUaGdsClient::certificateCheckInterval() const
{
Q_D(const QOpcUaGdsClient);
return d->certificateCheckInterval();
}
/*!
Sets the interval in milliseconds for updating the trust list from the server
to \a interval.
*/
void QOpcUaGdsClient::setTrustListUpdateInterval(int interval)
{
Q_D(QOpcUaGdsClient);
d->setTrustListUpdateInterval(interval);
}
/*!
Returns the interval in milliseconds for updating the trust list from the server.
*/
int QOpcUaGdsClient::trustListUpdateInterval() const
{
Q_D(const QOpcUaGdsClient);
return d->trustListUpdateInterval();
}
/*!
Returns the current error state.
*/
QOpcUaGdsClient::Error QOpcUaGdsClient::error() const
{
Q_D(const QOpcUaGdsClient);
return d->error();
}
/*!
Returns the current client state.
*/
QOpcUaGdsClient::State QOpcUaGdsClient::state() const
{
Q_D(const QOpcUaGdsClient);
return d->state();
}
/*!
Starts the client process.
After setting up all information,
the client can be started.
\list
\li setBackend
\li setEndpoing
\li setApplicationIdentity
\li setPkiConfiguration
\li setApplicationRecord
\li setCertificateSigingRequestPresets
\endlist
*/
void QOpcUaGdsClient::start()
{
Q_D(QOpcUaGdsClient);
return d->start();
}
/*!
Unregisters an application from the server.
This function can be used when an application has to be removed permanently from
the network. It does not need to be called when rebooting or shutting down.
*/
void QOpcUaGdsClient::unregisterApplication()
{
Q_D(QOpcUaGdsClient);
return d->unregisterApplication();
}
QOpcUaGdsClientPrivate::QOpcUaGdsClientPrivate()
: QObjectPrivate()
, m_certificateCheckTimer(new QTimer)
, m_trustListUpdateTimer(new QTimer)
{
m_certificateCheckTimer->setInterval(60*60*1000);
m_trustListUpdateTimer->setInterval(60*60*1000);
}
QOpcUaGdsClientPrivate::~QOpcUaGdsClientPrivate()
{
delete m_client;
delete m_directoryNode;
delete m_certificateGroupNode;
delete m_certificateTypesNode;
delete m_certificateFinishTimer;
delete m_certificateCheckTimer;
delete m_trustListUpdateTimer;
delete m_trustListNode;
}
void QOpcUaGdsClientPrivate::initializePrivateConnections()
{
Q_Q(QOpcUaGdsClient);
QObject::connect(m_certificateCheckTimer, SIGNAL(timeout()), q, SLOT(_q_certificateCheckTimeout()));
QObject::connect(m_trustListUpdateTimer, SIGNAL(timeout()), q, SLOT(_q_updateTrustList()));
}
void QOpcUaGdsClientPrivate::setEndpoint(const QOpcUaEndpointDescription &endpoint)
{
m_endpoint = endpoint;
}
const QOpcUaEndpointDescription &QOpcUaGdsClientPrivate::endpoint() const
{
return m_endpoint;
}
void QOpcUaGdsClientPrivate::setBackend(const QString &backend)
{
m_backend = backend;
}
const QString &QOpcUaGdsClientPrivate::backend() const
{
return m_backend;
}
QString QOpcUaGdsClientPrivate::applicationId() const
{
return m_appRecord.applicationId();
}
void QOpcUaGdsClientPrivate::setError(QOpcUaGdsClient::Error error)
{
Q_Q(QOpcUaGdsClient);
m_error = error;
setState(QOpcUaGdsClient::State::Error);
emit q->errorChanged(m_error);
}
void QOpcUaGdsClientPrivate::setState(QOpcUaGdsClient::State state)
{
Q_Q(QOpcUaGdsClient);
m_state = state;
emit q->stateChanged(state);
}
void QOpcUaGdsClientPrivate::start()
{
Q_Q(QOpcUaGdsClient);
if (m_backend.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Backend name not set";
setError(QOpcUaGdsClient::Error::InvalidBackend);
return;
}
if (!m_client) {
QOpcUaProvider provider;
setState(QOpcUaGdsClient::State::BackendInstantiated);
m_client = provider.createClient(m_backend);
if (!m_client) {
setError(QOpcUaGdsClient::Error::InvalidBackend);
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid backend";
return;
}
QObject::connect(m_client, &QOpcUaClient::namespaceArrayUpdated, q, [this]() {
setState(QOpcUaGdsClient::State::Connected);
this->resolveDirectoryNode();
});
QObject::connect(m_client, &QOpcUaClient::errorChanged, q, [this](QOpcUaClient::ClientError error) {
if (error == QOpcUaClient::InvalidUrl) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid URL";
setError(QOpcUaGdsClient::Error::InvalidEndpoint);
} else {
qCWarning(QT_OPCUA_GDSCLIENT) << "Connection error";
setError(QOpcUaGdsClient::Error::ConnectionError);
}
});
QObject::connect(m_client, &QOpcUaClient::disconnected, q, [this]() {
delete m_directoryNode;
m_directoryNode = nullptr;
m_directoryNodes.clear();
setState(QOpcUaGdsClient::State::Idle);
if (m_restartRequired) {
m_restartRequired = false;
this->start();
}
});
QObject::connect(m_client, &QOpcUaClient::connectError, q, [](QOpcUaErrorState *errorState) {
// Ignore all client side errors and continue
if (errorState->isClientSideError())
errorState->setIgnoreError();
});
}
setState(QOpcUaGdsClient::State::Connecting);
if (m_endpoint.endpointUrl().isEmpty()) {
setError(QOpcUaGdsClient::Error::InvalidEndpoint);
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid endpoint";
return;
}
QOpcUaKeyPair keyPair;
QFile keyFile(m_pkiConfig.privateKeyFile());
if (!keyFile.exists()) {
// File does not exist: Create a key
qCDebug(QT_OPCUA_GDSCLIENT) << "Creating a key";
keyPair.generateRsaKey(QOpcUaKeyPair::RsaKeyStrength::Bits1024);
if (!keyFile.open(QFile::WriteOnly | QFile::NewOnly)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open private key file" << keyFile.fileName() << "for writing:" << keyFile.errorString();
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
auto data = keyPair.privateKeyToByteArray(QOpcUaKeyPair::Cipher::Unencrypted, QString());
if (!keyFile.resize(data.size())) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to set file size";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
if (!keyFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to set permissions";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
keyFile.write(data);
keyFile.close();
} else {
qCDebug(QT_OPCUA_GDSCLIENT) << "Using private key" << keyFile.fileName();
if (!keyFile.open(QFile::ReadOnly)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open private key file" << keyFile.fileName() << "for reading:" << keyFile.errorString();
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
auto data = keyFile.readAll();
keyFile.close();
if (!keyPair.loadFromPemData(data)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load private key";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
if (!keyPair.hasPrivateKey()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Private key does not contain a private key";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
}
QFile certFile(m_pkiConfig.clientCertificateFile());
if (!certFile.exists()) {
qCDebug(QT_OPCUA_GDSCLIENT) << "Creating self-signed certificate in" << certFile.fileName();
auto csr = createSigningRequest();
csr.setEncoding(QOpcUaX509CertificateSigningRequest::Encoding::DER);
auto selfSigned = csr.createSelfSignedCertificate(keyPair);
if (!certFile.open(QFile::WriteOnly | QFile::NewOnly)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open certificate file" << certFile.fileName() << "for writing:" << certFile.errorString();
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
if (!certFile.resize(selfSigned.size())) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to set file size 2";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
certFile.write(selfSigned);
certFile.close();
}
// Load persistent data
QSettings settings;
qCDebug(QT_OPCUA_GDSCLIENT) << "Using settings from" << settings.fileName();
const auto applicationId = settings.value(u"gds/applicationId"_s, QString()).toString();
if (applicationId.isEmpty())
qCInfo(QT_OPCUA_GDSCLIENT) << "No application ID in persistent storage";
else
m_appRecord.setApplicationId(applicationId);
m_client->setApplicationIdentity(m_appIdentitiy);
m_client->setPkiConfiguration(m_pkiConfig);
m_client->connectToEndpoint(m_endpoint);
}
void QOpcUaGdsClientPrivate::resolveDirectoryNode()
{
Q_Q(QOpcUaGdsClient);
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
delete m_directoryNode;
m_directoryNode = m_client->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::ObjectsFolder)); // ns=0;i=85
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Root node not found";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (m_gdsNamespaceIndex < 0) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Namespace not found";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
QObject::connect(m_directoryNode, &QOpcUaNode::resolveBrowsePathFinished, q,
[this, q](QList<QOpcUaBrowsePathTarget> targets,
QList<QOpcUaRelativePathElement> path,
QOpcUa::UaStatusCode statusCode) {
m_directoryNode->deleteLater();
m_directoryNode = nullptr;
if (path.size() != 1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid path size";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (path[0].targetName().name() != "Directory"_L1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid resolve name" << path[0].targetName().name();
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Resolving directory failed" << statusCode;
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (targets.size() != 1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid number of results";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (!targets[0].isFullyResolved()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Directory not fully resolved";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
m_directoryNode = m_client->node(targets[0].targetId());
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid directory node";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
auto handleResolve = [this](QList<QOpcUaBrowsePathTarget> targets,
QList<QOpcUaRelativePathElement> path,
QOpcUa::UaStatusCode statusCode) {
_q_handleResolveBrowsePathFinished(targets, path, statusCode);
};
QObject::connect(m_directoryNode, &QOpcUaNode::resolveBrowsePathFinished,
q, std::move(handleResolve));
auto handleDirNode = [this](QString methodNodeId, QVariant result,
QOpcUa::UaStatusCode statusCode) {
_q_handleDirectoryNodeMethodCallFinished(methodNodeId, result, statusCode);
};
QObject::connect(m_directoryNode, &QOpcUaNode::methodCallFinished,
q, std::move(handleDirNode));
qCDebug(QT_OPCUA_GDSCLIENT) << "Directory node resolved:" << m_directoryNode->nodeId();
this->resolveMethodNodes();
});
QOpcUaRelativePathElement pathElement(QOpcUaQualifiedName(m_gdsNamespaceIndex, u"Directory"_s),
QOpcUa::ReferenceTypeId::Organizes);
QList<QOpcUaRelativePathElement> browsePath { pathElement };
if (!m_directoryNode->resolveBrowsePath(browsePath)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to resolve directory node";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
delete m_directoryNode;
m_directoryNode = nullptr;
return;
}
}
void QOpcUaGdsClientPrivate::resolveMethodNodes()
{
// See OPC UA Specification 1.05 part 12 6.6.2 "Directory"
QOpcUaRelativePathElement pathElement(QOpcUaQualifiedName(m_gdsNamespaceIndex, {}),
QOpcUa::ReferenceTypeId::HasComponent);
QList<QOpcUaRelativePathElement> browsePath { pathElement };
// Resolve all needed nodes from the directory
for (const auto &key : std::as_const(elementsToResolve)) {
if (!m_directoryNodes.value(key).isEmpty())
continue; // Already resolved
auto target = browsePath[0].targetName();
target.setName(key);
browsePath[0].setTargetName(target);
if (!m_directoryNode->resolveBrowsePath(browsePath)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not resolve Directory node";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
}
}
void QOpcUaGdsClientPrivate::_q_handleResolveBrowsePathFinished(QList<QOpcUaBrowsePathTarget> targets, QList<QOpcUaRelativePathElement> path, QOpcUa::UaStatusCode statusCode) {
if (path.size() != 1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid path size";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (m_directoryNodes.contains(path[0].targetName().name()) || !elementsToResolve.contains(path[0].targetName().name())) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid resolve name" << path[0].targetName().name();
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Resolving directory failed" << statusCode;
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (targets.size() != 1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid number of results";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
if (!targets[0].isFullyResolved()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Directory not fully resolved";
setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound);
return;
}
m_directoryNodes[path[0].targetName().name()] = targets[0].targetId().nodeId();
if (elementsToResolve.size() == m_directoryNodes.size()) {
qCDebug(QT_OPCUA_GDSCLIENT) << "All symbols resolved";
if (m_appRecord.applicationId().isEmpty() || m_appRecord.applicationId() == "ns=0;i=0"_L1)
this->findRegisteredApplication();
else
this->getApplication();
}
}
void QOpcUaGdsClientPrivate::getApplication()
{
// See OPC UA Specification 1.05 part 12 6.6.9 "GetApplication"
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
if (!m_directoryNode->callMethod(m_directoryNodes[u"GetApplication"_s],
QList<std::pair<QVariant, QOpcUa::Types>> { std::make_pair(QVariant(m_appRecord.applicationId()), QOpcUa::Types::NodeId) })) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method GetApplication";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
}
void QOpcUaGdsClientPrivate::handleGetApplicationFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
if (statusCode == QOpcUa::BadNotFound) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No application with ID" << m_appRecord.applicationId() << "registered";
// Clear application ID and register
m_appRecord.setApplicationId(QString());
// Remove invalid id from settings
QSettings settings;
settings.remove("gds/applicationId"_L1);
settings.sync();
restartWithCredentials();
return;
}
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Get application failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
auto extensionObject = result.value<QOpcUaExtensionObject>();
if (!extensionObject.encodingTypeId().isEmpty()) {
if (extensionObject.encodingTypeId() != QOpcUa::nodeIdFromInteger(m_gdsNamespaceIndex, ApplicationRecordDataType_Encoding_DefaultBinary)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Unexpected return type:" << extensionObject.encodingTypeId();
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
auto buffer = extensionObject.encodedBody();
QOpcUaBinaryDataEncoding decoder(&buffer);
bool ok;
QOpcUaApplicationRecordDataType appRecord = decoder.decode<QOpcUaApplicationRecordDataType>(ok);
if (!ok) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to decode data";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
if (m_appRecord.applicationId() != appRecord.applicationId())
qCWarning(QT_OPCUA_GDSCLIENT) << "Returned application record contains a different application ID" << m_appRecord.applicationId() << appRecord.applicationId();
// FIXME: To be removed
appRecord.setApplicationId(m_appRecord.applicationId());
m_appRecord = appRecord;
qCInfo(QT_OPCUA_GDSCLIENT) << "Reusing application ID" << m_appRecord.applicationId() << "which is already registered at the server";
}
getCertificateGroups();
}
void QOpcUaGdsClientPrivate::findRegisteredApplication()
{
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
if (!m_directoryNode->callMethod(m_directoryNodes[u"FindApplications"_s],
QList<std::pair<QVariant, QOpcUa::Types>> { std::make_pair(QVariant(m_appRecord.applicationUri()), QOpcUa::Types::String) })) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
}
void QOpcUaGdsClientPrivate::handleFindApplicationsFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Find application failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
auto extensionObject = result.value<QOpcUaExtensionObject>();
if (extensionObject.encodingTypeId().isEmpty()) {
qCInfo(QT_OPCUA_GDSCLIENT) << "No application with URI" << m_appIdentitiy.applicationUri() << "registered";
this->registerApplication();
return;
}
if (extensionObject.encodingTypeId() != QOpcUa::nodeIdFromInteger(m_gdsNamespaceIndex, ApplicationRecordDataType_Encoding_DefaultBinary)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Unexpected return type:" << extensionObject.encodingTypeId();
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
auto buffer = extensionObject.encodedBody();
QOpcUaBinaryDataEncoding decoder(&buffer);
bool ok;
QOpcUaApplicationRecordDataType appRecord = decoder.decode<QOpcUaApplicationRecordDataType>(ok);
if (!ok) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to decode data";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
m_appRecord = appRecord;
qCInfo(QT_OPCUA_GDSCLIENT) << "Reusing application ID" << appRecord.applicationId() << "registered for URI" << appRecord.applicationUri();
getCertificateGroups();
}
void QOpcUaGdsClientPrivate::registerApplication()
{
if (!m_appRecord.applicationId().isEmpty() && m_appRecord.applicationId() != "ns=0;i=0"_L1)
return;
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
QByteArray buffer;
QOpcUaBinaryDataEncoding encoder(&buffer);
if (!encoder.encode(m_appRecord)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to encode body";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
QOpcUaExtensionObject parameter;
parameter.setBinaryEncodedBody(buffer, QOpcUa::nodeIdFromInteger(m_gdsNamespaceIndex, ApplicationRecordDataType_Encoding_DefaultBinary));
if (!m_directoryNode->callMethod(m_directoryNodes[u"RegisterApplication"_s],
QList<QOpcUa::TypedVariant> { QOpcUa::TypedVariant(parameter, QOpcUa::ExtensionObject) })) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method RegisterApplication";
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
}
}
void QOpcUaGdsClientPrivate::handleRegisterApplicationFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
if (statusCode != QOpcUa::Good) {
qCDebug(QT_OPCUA_GDSCLIENT) << "Requesting user credentials";
if (statusCode == QOpcUa::BadUserAccessDenied && m_client->authenticationInformation().authenticationType() == QOpcUaUserTokenPolicy::Anonymous) {
restartWithCredentials();
return;
}
qCWarning(QT_OPCUA_GDSCLIENT) << "Register application failed with" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToRegisterApplication);
return;
}
m_appRecord.setApplicationId(result.toString());
QSettings settings;
settings.setValue(u"gds/applicationId"_s, m_appRecord.applicationId());
settings.sync();
qCInfo(QT_OPCUA_GDSCLIENT) << "Registered application with id" << m_appRecord.applicationId();
getCertificateGroups();
}
void QOpcUaGdsClientPrivate::getCertificateGroups()
{
// OPC UA Specification Version 1.05 Part 12 Chapter 7.9.7 GetCertificateGroups
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QList<QOpcUa::TypedVariant> arguments;
arguments.push_back(QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId));
if (!m_directoryNode->callMethod(m_directoryNodes[u"GetCertificateGroups"_s], arguments)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call GetCertificateGroups";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
}
}
void QOpcUaGdsClientPrivate::handleGetCertificateGroupsFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
Q_Q(QOpcUaGdsClient);
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate groups failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
m_certificateGroups = result.value<QStringList>();
qCDebug(QT_OPCUA_GDSCLIENT) << "Certificate groups:" << m_certificateGroups;
emit q->certificateGroupsReceived(m_certificateGroups);
resolveCertificateTypes();
}
void QOpcUaGdsClientPrivate::resolveCertificateTypes()
{
Q_Q(QOpcUaGdsClient);
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (m_certificateGroups.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate groups is empty";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
delete m_certificateGroupNode;
m_certificateGroupNode = m_client->node(m_certificateGroups[0]);
if (!m_certificateGroupNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate node failed";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QObject::connect(m_certificateGroupNode, &QOpcUaNode::resolveBrowsePathFinished,
q, [this](QList<QOpcUaBrowsePathTarget> targets,
QList<QOpcUaRelativePathElement> path,
QOpcUa::UaStatusCode statusCode) {
if (path.size() != 1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid path size";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (path[0].targetName().name() != "CertificateTypes"_L1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid resolve name" << path[0].targetName().name();
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Resolving certificate types failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (targets.size() != 1) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid number of results";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (!targets[0].isFullyResolved()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate types not fully resolved";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
m_certificateTypesNodeId = targets[0].targetId().nodeId();
this->getCertificateTypes();
});
QOpcUaRelativePathElement pathElement(QOpcUaQualifiedName(0, u"CertificateTypes"_s),
QOpcUa::ReferenceTypeId::Unspecified);
QList<QOpcUaRelativePathElement> browsePath { pathElement };
if (!m_certificateGroupNode->resolveBrowsePath(browsePath)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not resolve certificate type";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
}
void QOpcUaGdsClientPrivate::getCertificateTypes()
{
Q_Q(QOpcUaGdsClient);
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (m_certificateTypesNodeId.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate types node id is empty";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
delete m_certificateTypesNode;
m_certificateTypesNode = m_client->node(m_certificateTypesNodeId);
if (!m_certificateTypesNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate types node failed";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QObject::connect(m_certificateTypesNode, &QOpcUaNode::attributeUpdated,
q, [this](QOpcUa::NodeAttribute attr, QVariant value) {
if (attr == QOpcUa::NodeAttribute::Value)
qCWarning(QT_OPCUA_GDSCLIENT) << "possible certificate types" << value;
registrationDone();
});
if (!m_certificateTypesNode->readValueAttribute()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not read certificate types";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
}
void QOpcUaGdsClientPrivate::registrationDone()
{
Q_Q(QOpcUaGdsClient);
setState(QOpcUaGdsClient::State::ApplicationRegistered);
emit q->applicationRegistered();
m_certificateCheckTimer->start();
_q_certificateCheckTimeout(); // Force a check now
m_trustListUpdateTimer->start();
_q_updateTrustList(); // Force a check now
}
void QOpcUaGdsClientPrivate::getCertificateStatus()
{
// OPC UA Specification Version 1.05 Part 12 Chapter 7.9.10 GetCertificateStatus
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (m_certificateGroups.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No certificate groups received";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QString certificateType = u"ns=0;i=0"_s; // null node
QList<QOpcUa::TypedVariant> arguments;
arguments.push_back(QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId));
// Let the server choose the certificate group id
arguments.push_back(QOpcUa::TypedVariant(m_certificateGroups[0], QOpcUa::NodeId));
// Let the server choose the certificate type id
arguments.push_back(QOpcUa::TypedVariant(certificateType, QOpcUa::NodeId));
if (!m_directoryNode->callMethod(m_directoryNodes[u"GetCertificateStatus"_s], arguments)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call GetCertificateStatus";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
}
}
void QOpcUaGdsClientPrivate::handleGetCertificateStatusFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
Q_Q(QOpcUaGdsClient);
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate status failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
if (result.toBool()) {
qCInfo(QT_OPCUA_GDSCLIENT) << "Certificate needs update";
emit q->certificateUpdateRequired();
startCertificateRequest();
}
}
void QOpcUaGdsClientPrivate::startCertificateRequest()
{
// OPC UA Specification Version 1.05 Part 12 Chapter 7.9.3 StartSigningRequest
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QOpcUaKeyPair keyPair;
QFile keyFile(m_pkiConfig.privateKeyFile());
qCDebug(QT_OPCUA_GDSCLIENT) << "Using private key" << keyFile.fileName();
if (!keyFile.open(QFile::ReadOnly)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open private key file" << keyFile.fileName() << "for reading:" << keyFile.errorString();
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
auto data = keyFile.readAll();
keyFile.close();
if (!keyPair.loadFromPemData(data)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load private key";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
if (!keyPair.hasPrivateKey()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Private key does not contain a private key";
setError(QOpcUaGdsClient::Error::ConnectionError);
return;
}
auto csr = createSigningRequest();
csr.setEncoding(QOpcUaX509CertificateSigningRequest::Encoding::DER);
const auto csrData = csr.createRequest(keyPair);
QList<QOpcUa::TypedVariant> arguments;
arguments.push_back(QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId));
// Let the server choose the certificate group id
arguments.push_back(QOpcUa::TypedVariant(u"ns=0;i=0"_s, QOpcUa::NodeId));
// Let the server choose the certificate type id
arguments.push_back(QOpcUa::TypedVariant(u"ns=0;i=0"_s, QOpcUa::NodeId));
arguments.push_back(QOpcUa::TypedVariant(csrData, QOpcUa::ByteString));
if (!m_directoryNode->callMethod(m_directoryNodes[u"StartSigningRequest"_s], arguments)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call StartSigningRequest";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
}
}
void QOpcUaGdsClientPrivate::handleStartSigningRequestFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
Q_Q(QOpcUaGdsClient);
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
m_certificateRequestId = result.toString();
if (!m_certificateFinishTimer)
m_certificateFinishTimer = new QTimer;
m_certificateFinishTimer->setInterval(2000);
m_certificateFinishTimer->setSingleShot(true);
QObject::connect(m_certificateFinishTimer, &QTimer::timeout, q, [this]() {
this->finishCertificateRequest();
});
m_certificateFinishTimer->start();
}
void QOpcUaGdsClientPrivate::finishCertificateRequest()
{
// OPC UA Specification Version 1.05 Part 12 Chapter 7.9.5 FinishRequest
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (m_certificateRequestId.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No certificate request id";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QList<QOpcUa::TypedVariant> arguments;
arguments.push_back(QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId));
arguments.push_back(QOpcUa::TypedVariant(m_certificateRequestId, QOpcUa::NodeId));
if (!m_directoryNode->callMethod(m_directoryNodes[u"FinishRequest"_s], arguments)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call FinishRequest";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
}
}
void QOpcUaGdsClientPrivate::handleFinishRequestFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
Q_Q(QOpcUaGdsClient);
if (statusCode == QOpcUa::BadNothingToDo) {
// Server not finished yet: Try again later
m_certificateFinishTimer->setInterval(m_certificateFinishTimer->interval() * 2);
m_certificateFinishTimer->start();
qCWarning(QT_OPCUA_GDSCLIENT) << "Server not finished yet: Trying again in" << m_certificateFinishTimer->interval() / 1000 << "s";
return;
}
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
m_certificateRequestId.clear();
const auto resultList = result.toList();
if (resultList.size() != 3) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Expected list of 3 results but got" << resultList.size();
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
const auto certificate = resultList[0].toByteArray();
//const auto privateKey = resultList[1].toByteArray();
const auto issuerList = resultList[2].toByteArray();
if (certificate.isEmpty() || issuerList.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Certificates are empty";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
qCInfo(QT_OPCUA_GDSCLIENT) << "Received new certificate" << certificate;
QFile certificateFile(m_pkiConfig.clientCertificateFile());
if (!certificateFile.open(QFile::WriteOnly)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store certificate";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
if (certificateFile.write(certificate) != certificate.size()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store certificate data";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
certificateFile.close();
// FIMXE: How to store this?
QTemporaryFile issuerFile(m_pkiConfig.issuerListDirectory() + "/XXXXXX.der"_L1);
if (!issuerFile.open()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store issuer certificates to" << m_pkiConfig.issuerListDirectory();
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
if (issuerFile.write(issuerList) != issuerList.size()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store certificate data";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
qCDebug(QT_OPCUA_GDSCLIENT) << "issuer list stored to" << issuerFile.fileName();
issuerFile.close();
issuerFile.setAutoRemove(false);
emit q->certificateUpdated();
}
void QOpcUaGdsClientPrivate::unregisterApplication()
{
Q_Q(QOpcUaGdsClient);
if (m_appRecord.applicationId().isEmpty() || m_appRecord.applicationId() == "ns=0;i=0"_L1) {
emit q->unregistered();
return;
}
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication);
return;
}
if (!m_directoryNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node";
setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication);
return;
}
if (!m_directoryNode->callMethod(m_directoryNodes["UnregisterApplication"_L1],
QList<QOpcUa::TypedVariant> { QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId) })) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method UnregisterApplication";
setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication);
}
}
void QOpcUaGdsClientPrivate::handleUnregisterApplicationFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
Q_Q(QOpcUaGdsClient);
Q_UNUSED(result);
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Unregister application failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication);
return;
}
m_client->disconnectFromEndpoint();
emit q->unregistered();
}
void QOpcUaGdsClientPrivate::setPkiConfiguration(const QOpcUaPkiConfiguration &pkiConfig)
{
m_pkiConfig = pkiConfig;
}
const QOpcUaPkiConfiguration &QOpcUaGdsClientPrivate::pkiConfiguration() const
{
return m_pkiConfig;
}
void QOpcUaGdsClientPrivate::setApplicationIdentity(const QOpcUaApplicationIdentity &appIdentity)
{
m_appIdentitiy = appIdentity;
}
const QOpcUaApplicationIdentity &QOpcUaGdsClientPrivate::applicationIdentity() const
{
return m_appIdentitiy;
}
void QOpcUaGdsClientPrivate::setApplicationRecord(const QOpcUaApplicationRecordDataType &appRecord)
{
m_appRecord = appRecord;
}
const QOpcUaApplicationRecordDataType &QOpcUaGdsClientPrivate::applicationRecord() const
{
return m_appRecord;
}
void QOpcUaGdsClientPrivate::setCertificateSigningRequestPresets(const QOpcUaX509DistinguishedName &dn, const QString &dns)
{
csrPresets.dn = dn;
csrPresets.dns = dns;
}
const QOpcUaX509DistinguishedName &QOpcUaGdsClientPrivate::distinguishedNameCertificateSigningRequestPreset() const
{
return csrPresets.dn;
}
const QString &QOpcUaGdsClientPrivate::dnsCertificateSigningRequestPreset() const
{
return csrPresets.dns;
}
void QOpcUaGdsClientPrivate::setCertificateCheckInterval(int interval)
{
m_certificateCheckTimer->setInterval(interval);
}
int QOpcUaGdsClientPrivate::certificateCheckInterval() const
{
return m_certificateCheckTimer->interval();
}
void QOpcUaGdsClientPrivate::setTrustListUpdateInterval(int interval)
{
m_trustListUpdateTimer->setInterval(interval);
}
int QOpcUaGdsClientPrivate::trustListUpdateInterval() const
{
return m_trustListUpdateTimer->interval();
}
QOpcUaGdsClient::Error QOpcUaGdsClientPrivate::error() const
{
return m_error;
}
QOpcUaGdsClient::State QOpcUaGdsClientPrivate::state() const
{
return m_state;
}
QOpcUaX509CertificateSigningRequest QOpcUaGdsClientPrivate::createSigningRequest() const
{
QOpcUaX509CertificateSigningRequest csr;
QOpcUaX509DistinguishedName dn = csrPresets.dn;
// Overwrite the application name because it has to match the identity
dn.setEntry(QOpcUaX509DistinguishedName::Type::CommonName, m_appIdentitiy.applicationName());
csr.setSubject(dn);
QOpcUaX509ExtensionSubjectAlternativeName *san = new QOpcUaX509ExtensionSubjectAlternativeName;
san->addEntry(QOpcUaX509ExtensionSubjectAlternativeName::Type::URI, m_appIdentitiy.applicationUri());
if (!csrPresets.dns.isEmpty())
san->addEntry(QOpcUaX509ExtensionSubjectAlternativeName::Type::DNS, csrPresets.dns);
san->setCritical(true);
csr.addExtension(san);
QOpcUaX509ExtensionBasicConstraints *bc = new QOpcUaX509ExtensionBasicConstraints;
bc->setCa(false);
bc->setCritical(true);
csr.addExtension(bc);
QOpcUaX509ExtensionKeyUsage *ku = new QOpcUaX509ExtensionKeyUsage;
ku->setCritical(true);
ku->setKeyUsage(QOpcUaX509ExtensionKeyUsage::KeyUsage::DigitalSignature);
ku->setKeyUsage(QOpcUaX509ExtensionKeyUsage::KeyUsage::NonRepudiation);
ku->setKeyUsage(QOpcUaX509ExtensionKeyUsage::KeyUsage::KeyEncipherment);
ku->setKeyUsage(QOpcUaX509ExtensionKeyUsage::KeyUsage::DataEncipherment);
ku->setKeyUsage(QOpcUaX509ExtensionKeyUsage::KeyUsage::CertificateSigning);
csr.addExtension(ku);
return csr;
}
void QOpcUaGdsClientPrivate::_q_handleDirectoryNodeMethodCallFinished(QString methodNodeId, QVariant result, QOpcUa::UaStatusCode statusCode)
{
if (methodNodeId == m_directoryNodes[u"UnregisterApplication"_s]) {
this->handleUnregisterApplicationFinished(result, statusCode);
} else if ( methodNodeId == m_directoryNodes[u"FinishRequest"_s]) {
this->handleFinishRequestFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"StartSigningRequest"_s]) {
this->handleStartSigningRequestFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"GetCertificateStatus"_s]) {
this->handleGetCertificateStatusFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"GetCertificateGroups"_s]) {
this->handleGetCertificateGroupsFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"RegisterApplication"_s]) {
this->handleRegisterApplicationFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"FindApplications"_s]) {
this->handleFindApplicationsFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"GetApplication"_s]) {
this->handleGetApplicationFinished(result, statusCode);
} else if (methodNodeId == m_directoryNodes[u"GetTrustList"_s]) {
this->handleGetTrustListFinished(result, statusCode);
} else {
qCWarning(QT_OPCUA_GDSCLIENT) << "Result unexpeced method call received" << methodNodeId;
}
}
void QOpcUaGdsClientPrivate::_q_certificateCheckTimeout()
{
Q_Q(QOpcUaGdsClient);
QFile file(m_pkiConfig.clientCertificateFile());
if (!file.open(QFile::ReadOnly)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load certificate from file" << m_pkiConfig.clientCertificateFile();
}
QSslCertificate cert(&file, QSsl::Der);
if (cert.isNull()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load certificate from file" << m_pkiConfig.clientCertificateFile();
return;
}
bool needsUpdate = false;
if (cert.isSelfSigned()) {
needsUpdate = true;
qCInfo(QT_OPCUA_GDSCLIENT) << "Certificate is self-signed: requesting update";
}
if (QDateTime::currentDateTime().addSecs(24*60*60) > cert.expiryDate()) {
needsUpdate = true;
qCInfo(QT_OPCUA_GDSCLIENT) << "Certificate expiry date is due: requesting update";
}
if (!needsUpdate)
getCertificateStatus();
if (needsUpdate) {
emit q->certificateUpdateRequired();
startCertificateRequest();
}
}
void QOpcUaGdsClientPrivate::_q_updateTrustList()
{
// OPC UA Specification Version 1.05 Part 12 Chapter 7.9.9 GetTrustList
if (!m_client || m_client->state() != QOpcUaClient::Connected) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No connection";
return;
}
if (!m_certificateGroupNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "No certificate group node";
return;
}
QList<QOpcUa::TypedVariant> arguments;
arguments.push_back(QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId));
arguments.push_back(QOpcUa::TypedVariant(m_certificateGroupNode->nodeId(), QOpcUa::NodeId));
if (!m_directoryNode->callMethod(m_directoryNodes[u"GetTrustList"_s], arguments)) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call GetTrustList";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
}
}
void QOpcUaGdsClientPrivate::handleGetTrustListFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode)
{
Q_Q(QOpcUaGdsClient);
if (statusCode != QOpcUa::Good) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Getting trust list node failed" << statusCode;
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
const auto trustListNodeId = result.toString();
if (trustListNodeId.isEmpty()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Trust list node id is empty";
setError(QOpcUaGdsClient::Error::FailedToGetCertificate);
return;
}
delete m_trustListNode;
m_trustListNode = m_client->node(trustListNodeId);
if (!m_trustListNode) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Trust list node failed";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
QObject::connect(m_trustListNode, &QOpcUaNode::attributeUpdated,
q, [q](QOpcUa::NodeAttribute attr, QVariant value) {
Q_UNUSED(value);
if (attr == QOpcUa::NodeAttribute::Value)
emit q->trustListUpdated();
});
if (!m_trustListNode->readValueAttribute()) {
qCWarning(QT_OPCUA_GDSCLIENT) << "Could not read trust list";
setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus);
return;
}
}
// When it is detected that authentication credentials are required this function
// is used to re-connect to the server after requesting credentials
void QOpcUaGdsClientPrivate::restartWithCredentials()
{
Q_Q(QOpcUaGdsClient);
QOpcUaAuthenticationInformation authInfo;
emit q->authenticationRequired(authInfo);
m_client->setAuthenticationInformation(authInfo);
qCInfo(QT_OPCUA_GDSCLIENT) << "Restarting connection with credentials";
m_restartRequired = true;
m_client->disconnectFromEndpoint();
}
QT_END_NAMESPACE
#include "moc_qopcuagdsclient.cpp"