1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qopcuagdsclient_p.h" |
5 | #include <QOpcUaProvider> |
6 | #include <QOpcUaExtensionObject> |
7 | #include <QOpcUaBinaryDataEncoding> |
8 | #include <QOpcUaErrorState> |
9 | #include <QOpcUaKeyPair> |
10 | #include <QOpcUaX509ExtensionSubjectAlternativeName> |
11 | #include <QOpcUaX509ExtensionBasicConstraints> |
12 | #include <QOpcUaX509ExtensionKeyUsage> |
13 | #include <QFile> |
14 | #include <QTimer> |
15 | #include <QTemporaryFile> |
16 | #include <QSettings> |
17 | #include <QSslCertificate> |
18 | #include <QtCore/qloggingcategory.h> |
19 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | Q_LOGGING_CATEGORY(QT_OPCUA_GDSCLIENT, "qt.opcua.gdsclient" ) |
23 | |
24 | static const QStringList elementsToResolve { |
25 | QLatin1String("GetApplication" ), |
26 | QLatin1String("FindApplications" ), |
27 | QLatin1String("RegisterApplication" ), |
28 | QLatin1String("UnregisterApplication" ), |
29 | QLatin1String("GetCertificateGroups" ), |
30 | QLatin1String("GetCertificateStatus" ), |
31 | QLatin1String("StartSigningRequest" ), |
32 | QLatin1String("FinishRequest" ), |
33 | QLatin1String("GetTrustList" ), |
34 | }; |
35 | |
36 | /*! |
37 | \class QOpcUaGdsClient |
38 | \inmodule QtOpcUa |
39 | \since 5.14 |
40 | |
41 | \brief Handles communication with the GDS Server. |
42 | |
43 | This class is currently available as a Technology Preview, and therefore the API |
44 | and functionality provided by the class may be subject to change at any time without |
45 | prior notice. |
46 | |
47 | This class handles all steps needed for communication with a GDS server. Provided with |
48 | information about the application it does registering with the server and managing |
49 | key/certificates. |
50 | |
51 | Only few details need to be known in order to take part in a secured network. |
52 | |
53 | First time registration requires administrative privileges using username and password |
54 | for authentication. All further authentications are application based, using the certificate |
55 | which was received first. |
56 | |
57 | Expecting the whole process to succeed, you have to wait for the applicationRegistered signal. |
58 | |
59 | Most of the setup structs have to be the same as for the connection with QOpcUaClient afterwards |
60 | and can be shared. |
61 | |
62 | Setting up a GDS client: |
63 | |
64 | \code |
65 | QOpcUaGdsClient c; |
66 | |
67 | // In case the credentials are needed |
68 | QObject::connect(&c, &QOpcUaGdsClient::authenticationRequired, [&](QOpcUaAuthenticationInformation &authInfo) { |
69 | authInfo.setUsernameAuthentication("root", "secret"); |
70 | }); |
71 | |
72 | // Await success |
73 | QObject::connect(&c, &QOpcUaGdsClient::applicationRegistered, [&]() { |
74 | qDebug() << "Application" << c.applicationId() << "registered"; |
75 | }); |
76 | |
77 | c.setBackend(...); |
78 | c.setEndpoint(...); |
79 | c.setApplicationIdentity(...); |
80 | c.setPkiConfiguration(...); |
81 | c.setApplicationRecord(...); |
82 | c.setCertificateSigningRequestPresets(...); |
83 | c.start(); |
84 | \endcode |
85 | */ |
86 | |
87 | /*! |
88 | \enum QOpcUaGdsClient::Error |
89 | |
90 | This enum is used to specify errors, that could happen during the registration |
91 | process. |
92 | |
93 | \value NoError |
94 | Everying is fine |
95 | \value InvalidBackend |
96 | The backend could not be instantiated. |
97 | The backend string given, does not match any backend or loading the plugin failed. |
98 | \value InvalidEndpoint |
99 | The given endpoint is not valid. |
100 | \value ConnectionError |
101 | The connection to the server endpoint failed. |
102 | \value DirectoryNodeNotFound |
103 | The directory node on the server could not be resolved |
104 | \value FailedToRegisterApplication |
105 | The registration of the application was not successful. |
106 | \value FailedToUnregisterApplication |
107 | The unregistration of the application was not successful. |
108 | \value FailedToGetCertificateStatus |
109 | The status of the current certificate could not be retrieved. |
110 | \value FailedToGetCertificate |
111 | A certificate could not be retrieved from the server. |
112 | */ |
113 | |
114 | /*! |
115 | \enum QOpcUaGdsClient::State |
116 | |
117 | This enum is used to specify the current state of the registration of the GDS |
118 | client. |
119 | |
120 | \value Idle |
121 | The client was not started yet. |
122 | \value BackendInstantiated |
123 | The backend was instantiated |
124 | \value Connecting |
125 | A connecting to the server is being made |
126 | \value Connected |
127 | The connection to the server endpoint was successful. |
128 | \value RegisteringApplication |
129 | The application is being registered with the server. |
130 | \value ApplicationRegistered |
131 | Registering the application with the server was successful. |
132 | \value Error |
133 | An error happened. See the return value of error() and the terminal output |
134 | for more details. |
135 | */ |
136 | |
137 | /*! |
138 | \fn QOpcUaGdsClient::stateChanged(State state) |
139 | |
140 | This signal is emitted when the internal state of the client changes. |
141 | The \a state indicates the new state. |
142 | */ |
143 | |
144 | /*! |
145 | \fn QOpcUaGdsClient::errorChanged(Error error) |
146 | |
147 | This signal is emitted when an \a error occurred. |
148 | */ |
149 | |
150 | /*! |
151 | \fn QOpcUaGdsClient::applicationRegistered() |
152 | |
153 | This signal is emitted when an application was registered successfully. |
154 | */ |
155 | |
156 | /*! |
157 | \fn QOpcUaGdsClient::certificateGroupsReceived(QStringList certificateGroups) |
158 | |
159 | This signal is emitted when the GDS client receives a new list of \a certificateGroups |
160 | for this application. |
161 | */ |
162 | |
163 | /*! |
164 | \fn QOpcUaGdsClient::certificateUpdateRequired() |
165 | |
166 | This signal is emitted when the GDS client detects that an update of the currently |
167 | used certificate is necessary. |
168 | |
169 | This could be caused by the server, requesting the client to update the certificate, |
170 | when the certificate's due date is met or if the certificate is self-signed. |
171 | |
172 | The certificate update is handled automatically. This signal is only for informational |
173 | purpose that an update is going to happen. |
174 | */ |
175 | |
176 | /*! |
177 | \fn QOpcUaGdsClient::certificateUpdated() |
178 | |
179 | This signal is emitted when the GDS client received a new certificate that was |
180 | stored on disk. |
181 | */ |
182 | |
183 | /*! |
184 | \fn QOpcUaGdsClient::unregistered() |
185 | |
186 | This signal is emitted when the GDS client has unregistered the application. |
187 | */ |
188 | |
189 | /*! |
190 | \fn QOpcUaGdsClient::trustListUpdated() |
191 | |
192 | This signal is emitted when the GDS client has received a new trust list from the |
193 | server and stored to disk. |
194 | */ |
195 | |
196 | /*! |
197 | \fn QOpcUaGdsClient::authenticationRequired(QOpcUaAuthenticationInformation &authInfo) |
198 | |
199 | This signal is emitted when the GDS client tries to do a first time authentication |
200 | with a server, that requires administrative privileges. |
201 | |
202 | \a authInfo has to be filled with valid authentication information. |
203 | This slot must not be used crossing thread boundaries. |
204 | */ |
205 | |
206 | /* |
207 | Step 1 |
208 | Create a self-signed certificate for the application. |
209 | Previously created key and certificate are reused if present. |
210 | |
211 | Step 2 |
212 | Connect to the given endpoint and wait for the namespace array to be updated. |
213 | |
214 | Step 3 |
215 | Find the root node of the GDS API |
216 | |
217 | Step 4 |
218 | From the root node find the "Directory" node where the method nodes are located. |
219 | - resolveDirectoryNode() |
220 | - lambda |
221 | |
222 | Step 5 |
223 | Resolve the method nodes that are being used. |
224 | - resolveMethodNodes() |
225 | - _q_handleResolveBrowsePathFinished() |
226 | |
227 | Step 6a |
228 | In case a previous appliation id is known, check if the application registration is still valid. |
229 | In case it is still valid, reuse it. |
230 | - getApplication() |
231 | - handleGetApplicationFinished() |
232 | |
233 | Step 6b |
234 | In case there is no valid application id known, try to find a previous registration by the application URI. |
235 | - findRegisteredApplication() |
236 | - handleFindApplicationsFinished() |
237 | |
238 | Step 6c |
239 | In case an application id was not found by the application URI, register the application. |
240 | - registerApplication() |
241 | - handleRegisterApplicationFinished() |
242 | |
243 | Step 7 |
244 | Get the certificate groups of the application |
245 | - getCertificateGroups() |
246 | - handleGetCertificateGroupsFinished() |
247 | - emit signal certificateGroupsReceived |
248 | |
249 | Step 8 |
250 | Resolve the certificate types from the certificate groups (CertificateGroup->CertificateTypes) |
251 | - resolveCertificateTypes() |
252 | - lambda |
253 | |
254 | Step 9 |
255 | Read certificate type value from the resolved nodes |
256 | - getCertificateTypes() |
257 | - lambda |
258 | |
259 | Step 10 |
260 | The application now is using a application id from 6a, 6b or 6c, resolved the certificates and is ready to interact with the GDS. |
261 | - registrationDone() |
262 | - emit signal applicationRegistered |
263 | -> Start a certificate check |
264 | -> Start a trust list update |
265 | |
266 | Depending on the certificate state, the certificate needs to be updated by the client. |
267 | This may happen due to |
268 | - the server requires it (GetCertificateStatus) |
269 | - the expiry date is due |
270 | - the certificate is self-signed from the initial connection |
271 | |
272 | Step 1 |
273 | A timer will trigger the certificate check regularly by triggering |
274 | - _q_certificateCheckTimeout() |
275 | - emit signal certificateUpdateRequired |
276 | |
277 | Step 2a |
278 | Check locally first, if a renew condition is met. |
279 | Otherwise ask the server. |
280 | |
281 | Step 2b |
282 | Ask the server if the certificate needs renewal |
283 | - getCertificateStatus() |
284 | - handleGetCertificateStatusFinished() |
285 | - emit signal certificateUpdateRequired |
286 | |
287 | Step 3 |
288 | Start a certificate signing request |
289 | - startCertificateRequest() |
290 | - handleStartSigningRequestFinished() |
291 | |
292 | Step 4 |
293 | Regularly ask the server for the certificate signing result |
294 | - finishCertificateRequest() |
295 | - handleFinishRequestFinished() |
296 | - emit signal certificateUpdated |
297 | This will return a new certificate. |
298 | */ |
299 | |
300 | /*! |
301 | Constructs a GDS client with \a parent as the parent object. |
302 | */ |
303 | QOpcUaGdsClient::QOpcUaGdsClient(QObject *parent) |
304 | : QObject(*(new QOpcUaGdsClientPrivate()), parent) |
305 | { |
306 | Q_D(QOpcUaGdsClient); |
307 | d->initializePrivateConnections(); |
308 | } |
309 | |
310 | /*! |
311 | Destructs a GDS client. |
312 | */ |
313 | QOpcUaGdsClient::~QOpcUaGdsClient() |
314 | { |
315 | |
316 | } |
317 | |
318 | /*! |
319 | Sets the backend to be used by the client to communicate with the server to \a backend. |
320 | |
321 | This function has to be called before starting the GDS client. |
322 | Changing this setting afterwards has no effect. |
323 | |
324 | \sa QOpcUaProvider::availableBackends() start() |
325 | */ |
326 | void QOpcUaGdsClient::setBackend(const QString &backend) |
327 | { |
328 | Q_D(QOpcUaGdsClient); |
329 | d->setBackend(backend); |
330 | } |
331 | |
332 | /*! |
333 | Returns the current backend setting. |
334 | |
335 | If the backend was changed after starting the client, it will return |
336 | the changed setting, but not the actually used instance. |
337 | */ |
338 | const QString &QOpcUaGdsClient::backend() const |
339 | { |
340 | Q_D(const QOpcUaGdsClient); |
341 | return d->backend(); |
342 | } |
343 | |
344 | /*! |
345 | Sets the endpoint to be used by the client to communicate with the server to \a endpoint. |
346 | |
347 | This function has to be called before starting the GDS client. |
348 | Changing this setting afterwards has no effect. |
349 | |
350 | Communication to a GDS server is only possible through an encrypted endpoint. |
351 | Using an unencrypted endpoint will fail. |
352 | */ |
353 | void QOpcUaGdsClient::setEndpoint(const QOpcUaEndpointDescription &endpoint) |
354 | { |
355 | Q_D(QOpcUaGdsClient); |
356 | d->setEndpoint(endpoint); |
357 | } |
358 | |
359 | /*! |
360 | Returns the current endpoint setting. |
361 | |
362 | If the endpoint was changed after starting the client, it will return |
363 | the changed setting, but not the actually used endpoint. |
364 | */ |
365 | const QOpcUaEndpointDescription &QOpcUaGdsClient::endpoint() const |
366 | { |
367 | Q_D(const QOpcUaGdsClient); |
368 | return d->endpoint(); |
369 | } |
370 | |
371 | /*! |
372 | Sets the PKI configuration \a pkiConfig to be used by the client. |
373 | |
374 | All certificates, keys and trust lists will be used from or stored to |
375 | the locations given. In order to use the certificate received from |
376 | the GDS, the same configuration has to be used with QOpcUaClient. |
377 | |
378 | This function has to be called before starting the GDS client. |
379 | Changing this setting afterwards has no effect. |
380 | */ |
381 | void QOpcUaGdsClient::setPkiConfiguration(const QOpcUaPkiConfiguration &pkiConfig) |
382 | { |
383 | Q_D(QOpcUaGdsClient); |
384 | d->setPkiConfiguration(pkiConfig); |
385 | } |
386 | |
387 | /*! |
388 | Returns the current pkiConfiguration. |
389 | */ |
390 | const QOpcUaPkiConfiguration &QOpcUaGdsClient::pkiConfiguration() const |
391 | { |
392 | Q_D(const QOpcUaGdsClient); |
393 | return d->pkiConfiguration(); |
394 | } |
395 | |
396 | /*! |
397 | Sets the application identity \a appIdentity to be used by the client. |
398 | |
399 | This identity is used to register with the GDS server. |
400 | This function has to be called before starting the GDS client. |
401 | Changing this setting afterwards has no effect. |
402 | */ |
403 | void QOpcUaGdsClient::setApplicationIdentity(const QOpcUaApplicationIdentity &appIdentity) |
404 | { |
405 | Q_D(QOpcUaGdsClient); |
406 | d->setApplicationIdentity(appIdentity); |
407 | } |
408 | |
409 | /*! |
410 | Returns the current applicationIdentity. |
411 | */ |
412 | const QOpcUaApplicationIdentity &QOpcUaGdsClient::applicationIdentity() const |
413 | { |
414 | Q_D(const QOpcUaGdsClient); |
415 | return d->applicationIdentity(); |
416 | } |
417 | |
418 | /*! |
419 | Sets the application record data \a appRecord to be used by the client. |
420 | |
421 | This data is used to register with the GDS server. |
422 | This function has to be called before starting the GDS client. |
423 | |
424 | Most of the data is the same as in the application identity. |
425 | After registration the assigned application id can be retrieved. |
426 | |
427 | \sa setApplicationIdentity |
428 | */ |
429 | void QOpcUaGdsClient::setApplicationRecord(const QOpcUaApplicationRecordDataType &appRecord) |
430 | { |
431 | Q_D(QOpcUaGdsClient); |
432 | d->setApplicationRecord(appRecord); |
433 | } |
434 | |
435 | /*! |
436 | Returns the application record data that is used by the client. |
437 | */ |
438 | const QOpcUaApplicationRecordDataType &QOpcUaGdsClient::applicationRecord() const |
439 | { |
440 | Q_D(const QOpcUaGdsClient); |
441 | return d->applicationRecord(); |
442 | } |
443 | |
444 | /*! |
445 | Returns the application id assigned by the server. |
446 | |
447 | Is is a shortcut to receive the application id from the application record data. |
448 | |
449 | \sa applicationRecord() |
450 | */ |
451 | QString QOpcUaGdsClient::applicationId() const |
452 | { |
453 | Q_D(const QOpcUaGdsClient); |
454 | return d->applicationId(); |
455 | } |
456 | |
457 | /*! |
458 | Sets the presets for certificate siging requests; the distinguished name \a dn and |
459 | the DNS string \a dns. |
460 | |
461 | When creating a certificate signing request some additional information is needed, |
462 | that is not provided by the application identity. |
463 | |
464 | This function has to be called before starting the GDS client. |
465 | |
466 | \sa setApplicationIdentity() |
467 | */ |
468 | void QOpcUaGdsClient::setCertificateSigningRequestPresets(const QOpcUaX509DistinguishedName &dn, const QString &dns) |
469 | { |
470 | Q_D(QOpcUaGdsClient); |
471 | d->setCertificateSigningRequestPresets(dn, dns); |
472 | } |
473 | |
474 | /*! |
475 | Returns the distinguished name preset for certificate siging requests. |
476 | */ |
477 | const QOpcUaX509DistinguishedName &QOpcUaGdsClient::distinguishedNameCertificateSigningRequestPreset() const |
478 | { |
479 | Q_D(const QOpcUaGdsClient); |
480 | return d->distinguishedNameCertificateSigningRequestPreset(); |
481 | } |
482 | |
483 | /*! |
484 | Returns the DNS preset for certificate siging requests. |
485 | */ |
486 | const QString &QOpcUaGdsClient::dnsCertificateSigningRequestPreset() const |
487 | { |
488 | Q_D(const QOpcUaGdsClient); |
489 | return d->dnsCertificateSigningRequestPreset(); |
490 | } |
491 | |
492 | /*! |
493 | Sets the interval in milliseconds for checking the validity of the client certificate |
494 | to \a interval. |
495 | */ |
496 | void QOpcUaGdsClient::setCertificateCheckInterval(int interval) |
497 | { |
498 | Q_D(QOpcUaGdsClient); |
499 | d->setCertificateCheckInterval(interval); |
500 | } |
501 | |
502 | /*! |
503 | Returns the interval in milliseconds for checking the validity of the client certificate. |
504 | */ |
505 | int QOpcUaGdsClient::certificateCheckInterval() const |
506 | { |
507 | Q_D(const QOpcUaGdsClient); |
508 | return d->certificateCheckInterval(); |
509 | } |
510 | |
511 | /*! |
512 | Sets the interval in milliseconds for updating the trust list from the server |
513 | to \a interval. |
514 | */ |
515 | void QOpcUaGdsClient::setTrustListUpdateInterval(int interval) |
516 | { |
517 | Q_D(QOpcUaGdsClient); |
518 | d->setTrustListUpdateInterval(interval); |
519 | } |
520 | |
521 | /*! |
522 | Returns the interval in milliseconds for updating the trust list from the server. |
523 | */ |
524 | int QOpcUaGdsClient::trustListUpdateInterval() const |
525 | { |
526 | Q_D(const QOpcUaGdsClient); |
527 | return d->trustListUpdateInterval(); |
528 | } |
529 | |
530 | /*! |
531 | Returns the current error state. |
532 | */ |
533 | QOpcUaGdsClient::Error QOpcUaGdsClient::error() const |
534 | { |
535 | Q_D(const QOpcUaGdsClient); |
536 | return d->error(); |
537 | } |
538 | |
539 | /*! |
540 | Returns the current client state. |
541 | */ |
542 | QOpcUaGdsClient::State QOpcUaGdsClient::state() const |
543 | { |
544 | Q_D(const QOpcUaGdsClient); |
545 | return d->state(); |
546 | } |
547 | |
548 | /*! |
549 | Starts the client process. |
550 | |
551 | After setting up all information, |
552 | the client can be started. |
553 | |
554 | \list |
555 | \li setBackend |
556 | \li setEndpoing |
557 | \li setApplicationIdentity |
558 | \li setPkiConfiguration |
559 | \li setApplicationRecord |
560 | \li setCertificateSigingRequestPresets |
561 | \endlist |
562 | */ |
563 | void QOpcUaGdsClient::start() |
564 | { |
565 | Q_D(QOpcUaGdsClient); |
566 | return d->start(); |
567 | } |
568 | |
569 | /*! |
570 | Unregisters an application from the server. |
571 | |
572 | This function can be used when an application has to be removed permanently from |
573 | the network. It does not need to be called when rebooting or shutting down. |
574 | */ |
575 | void QOpcUaGdsClient::unregisterApplication() |
576 | { |
577 | Q_D(QOpcUaGdsClient); |
578 | return d->unregisterApplication(); |
579 | } |
580 | |
581 | QOpcUaGdsClientPrivate::QOpcUaGdsClientPrivate() |
582 | : QObjectPrivate() |
583 | , m_certificateCheckTimer(new QTimer) |
584 | , m_trustListUpdateTimer(new QTimer) |
585 | { |
586 | m_certificateCheckTimer->setInterval(60*60*1000); |
587 | m_trustListUpdateTimer->setInterval(60*60*1000); |
588 | } |
589 | |
590 | QOpcUaGdsClientPrivate::~QOpcUaGdsClientPrivate() |
591 | { |
592 | delete m_client; |
593 | delete m_directoryNode; |
594 | delete m_certificateGroupNode; |
595 | delete m_certificateTypesNode; |
596 | delete m_certificateFinishTimer; |
597 | delete m_certificateCheckTimer; |
598 | delete m_trustListUpdateTimer; |
599 | delete m_trustListNode; |
600 | } |
601 | |
602 | void QOpcUaGdsClientPrivate::initializePrivateConnections() |
603 | { |
604 | Q_Q(QOpcUaGdsClient); |
605 | QObject::connect(sender: m_certificateCheckTimer, SIGNAL(timeout()), receiver: q, SLOT(_q_certificateCheckTimeout())); |
606 | QObject::connect(sender: m_trustListUpdateTimer, SIGNAL(timeout()), receiver: q, SLOT(_q_updateTrustList())); |
607 | } |
608 | |
609 | void QOpcUaGdsClientPrivate::setEndpoint(const QOpcUaEndpointDescription &endpoint) |
610 | { |
611 | m_endpoint = endpoint; |
612 | } |
613 | |
614 | const QOpcUaEndpointDescription &QOpcUaGdsClientPrivate::endpoint() const |
615 | { |
616 | return m_endpoint; |
617 | } |
618 | |
619 | void QOpcUaGdsClientPrivate::setBackend(const QString &backend) |
620 | { |
621 | m_backend = backend; |
622 | } |
623 | |
624 | const QString &QOpcUaGdsClientPrivate::backend() const |
625 | { |
626 | return m_backend; |
627 | } |
628 | |
629 | QString QOpcUaGdsClientPrivate::applicationId() const |
630 | { |
631 | return m_appRecord.applicationId(); |
632 | } |
633 | |
634 | void QOpcUaGdsClientPrivate::setError(QOpcUaGdsClient::Error error) |
635 | { |
636 | Q_Q(QOpcUaGdsClient); |
637 | m_error = error; |
638 | setState(QOpcUaGdsClient::State::Error); |
639 | emit q->errorChanged(error: m_error); |
640 | } |
641 | |
642 | void QOpcUaGdsClientPrivate::setState(QOpcUaGdsClient::State state) |
643 | { |
644 | Q_Q(QOpcUaGdsClient); |
645 | m_state = state; |
646 | emit q->stateChanged(state); |
647 | } |
648 | |
649 | void QOpcUaGdsClientPrivate::start() |
650 | { |
651 | Q_Q(QOpcUaGdsClient); |
652 | |
653 | if (m_backend.isEmpty()) { |
654 | qCWarning(QT_OPCUA_GDSCLIENT) << "Backend name not set" ; |
655 | setError(QOpcUaGdsClient::Error::InvalidBackend); |
656 | return; |
657 | } |
658 | |
659 | if (!m_client) { |
660 | QOpcUaProvider provider; |
661 | setState(QOpcUaGdsClient::State::BackendInstantiated); |
662 | m_client = provider.createClient(backend: m_backend); |
663 | if (!m_client) { |
664 | setError(QOpcUaGdsClient::Error::InvalidBackend); |
665 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid backend" ; |
666 | return; |
667 | } |
668 | |
669 | QObject::connect(sender: m_client, signal: &QOpcUaClient::namespaceArrayUpdated, slot: [this]() { |
670 | setState(QOpcUaGdsClient::State::Connected); |
671 | this->resolveDirectoryNode(); |
672 | }); |
673 | |
674 | QObject::connect(sender: m_client, signal: &QOpcUaClient::errorChanged, context: q, slot: [this](QOpcUaClient::ClientError error) { |
675 | if (error == QOpcUaClient::InvalidUrl) { |
676 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid URL" ; |
677 | setError(QOpcUaGdsClient::Error::InvalidEndpoint); |
678 | } else { |
679 | qCWarning(QT_OPCUA_GDSCLIENT) << "Connection error" ; |
680 | setError(QOpcUaGdsClient::Error::ConnectionError); |
681 | } |
682 | }); |
683 | |
684 | QObject::connect(sender: m_client, signal: &QOpcUaClient::disconnected, context: q, slot: [this]() { |
685 | delete m_directoryNode; |
686 | m_directoryNode = nullptr; |
687 | m_directoryNodes.clear(); |
688 | setState(QOpcUaGdsClient::State::Idle); |
689 | if (m_restartRequired) { |
690 | m_restartRequired = false; |
691 | this->start(); |
692 | } |
693 | }); |
694 | |
695 | QObject::connect(sender: m_client, signal: &QOpcUaClient::connectError, slot: [](QOpcUaErrorState *errorState) { |
696 | // Ignore all client side errors and continue |
697 | if (errorState->isClientSideError()) |
698 | errorState->setIgnoreError(); |
699 | }); |
700 | } |
701 | |
702 | setState(QOpcUaGdsClient::State::Connecting); |
703 | if (m_endpoint.endpointUrl().isEmpty()) { |
704 | setError(QOpcUaGdsClient::Error::InvalidEndpoint); |
705 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid endpoint" ; |
706 | return; |
707 | } |
708 | |
709 | QOpcUaKeyPair keyPair; |
710 | QFile keyFile(m_pkiConfig.privateKeyFile()); |
711 | |
712 | if (!keyFile.exists()) { |
713 | // File does not exist: Create a key |
714 | qCDebug(QT_OPCUA_GDSCLIENT) << "Creating a key" ; |
715 | keyPair.generateRsaKey(strength: QOpcUaKeyPair::RsaKeyStrength::Bits1024); |
716 | |
717 | if (!keyFile.open(flags: QFile::WriteOnly | QFile::NewOnly)) { |
718 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open private key file" << keyFile.fileName() << "for writing:" << keyFile.errorString(); |
719 | setError(QOpcUaGdsClient::Error::ConnectionError); |
720 | return; |
721 | } |
722 | |
723 | auto data = keyPair.privateKeyToByteArray(cipher: QOpcUaKeyPair::Cipher::Unencrypted, password: QString()); |
724 | |
725 | if (!keyFile.resize(sz: data.size())) { |
726 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to set file size" ; |
727 | setError(QOpcUaGdsClient::Error::ConnectionError); |
728 | return; |
729 | } |
730 | |
731 | if (!keyFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner)) { |
732 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to set permissions" ; |
733 | setError(QOpcUaGdsClient::Error::ConnectionError); |
734 | return; |
735 | } |
736 | |
737 | keyFile.write(data); |
738 | keyFile.close(); |
739 | } else { |
740 | qCDebug(QT_OPCUA_GDSCLIENT) << "Using private key" << keyFile.fileName(); |
741 | |
742 | if (!keyFile.open(flags: QFile::ReadOnly)) { |
743 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open private key file" << keyFile.fileName() << "for reading:" << keyFile.errorString(); |
744 | setError(QOpcUaGdsClient::Error::ConnectionError); |
745 | return; |
746 | } |
747 | |
748 | auto data = keyFile.readAll(); |
749 | keyFile.close(); |
750 | |
751 | if (!keyPair.loadFromPemData(data)) { |
752 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load private key" ; |
753 | setError(QOpcUaGdsClient::Error::ConnectionError); |
754 | return; |
755 | } |
756 | |
757 | if (!keyPair.hasPrivateKey()) { |
758 | qCWarning(QT_OPCUA_GDSCLIENT) << "Private key does not contain a private key" ; |
759 | setError(QOpcUaGdsClient::Error::ConnectionError); |
760 | return; |
761 | } |
762 | } |
763 | |
764 | QFile certFile(m_pkiConfig.clientCertificateFile()); |
765 | if (!certFile.exists()) { |
766 | qCDebug(QT_OPCUA_GDSCLIENT) << "Creating self-signed certificate in" << certFile.fileName(); |
767 | |
768 | auto csr = createSigningRequest(); |
769 | csr.setEncoding(QOpcUaX509CertificateSigningRequest::Encoding::DER); |
770 | auto selfSigned = csr.createSelfSignedCertificate(privateKey: keyPair); |
771 | |
772 | if (!certFile.open(flags: QFile::WriteOnly | QFile::NewOnly)) { |
773 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open certificate file" << certFile.fileName() << "for writing:" << certFile.errorString(); |
774 | setError(QOpcUaGdsClient::Error::ConnectionError); |
775 | return; |
776 | } |
777 | |
778 | if (!certFile.resize(sz: selfSigned.size())) { |
779 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to set file size 2" ; |
780 | setError(QOpcUaGdsClient::Error::ConnectionError); |
781 | return; |
782 | } |
783 | |
784 | certFile.write(data: selfSigned); |
785 | certFile.close(); |
786 | } |
787 | |
788 | // Load persistent data |
789 | QSettings settings; |
790 | qCDebug(QT_OPCUA_GDSCLIENT) << "Using settings from" << settings.fileName(); |
791 | const auto applicationId = settings.value(key: QLatin1String("gds/applicationId" ), defaultValue: QString()).toString(); |
792 | if (applicationId.isEmpty()) |
793 | qCInfo(QT_OPCUA_GDSCLIENT) << "No application ID in persistent storage" ; |
794 | else |
795 | m_appRecord.setApplicationId(applicationId); |
796 | |
797 | m_client->setApplicationIdentity(m_appIdentitiy); |
798 | m_client->setPkiConfiguration(m_pkiConfig); |
799 | m_client->connectToEndpoint(endpoint: m_endpoint); |
800 | } |
801 | |
802 | void QOpcUaGdsClientPrivate::resolveDirectoryNode() |
803 | { |
804 | Q_Q(QOpcUaGdsClient); |
805 | |
806 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
807 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
808 | return; |
809 | } |
810 | |
811 | delete m_directoryNode; |
812 | m_directoryNode = m_client->node(nodeId: QOpcUa::namespace0Id(id: QOpcUa::NodeIds::Namespace0::ObjectsFolder)); // ns=0;i=85 |
813 | if (!m_directoryNode) { |
814 | qCWarning(QT_OPCUA_GDSCLIENT) << "Root node not found" ; |
815 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
816 | return; |
817 | } |
818 | |
819 | m_gdsNamespaceIndex = m_client->namespaceArray().indexOf(t: QLatin1String("http://opcfoundation.org/UA/GDS/" )); |
820 | if (m_gdsNamespaceIndex < 0) { |
821 | qCWarning(QT_OPCUA_GDSCLIENT) << "Namespace not found" ; |
822 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
823 | return; |
824 | } |
825 | |
826 | QObject::connect(sender: m_directoryNode, signal: &QOpcUaNode::resolveBrowsePathFinished, slot: [this, q](QList<QOpcUaBrowsePathTarget> targets, |
827 | QList<QOpcUaRelativePathElement> path, |
828 | QOpcUa::UaStatusCode statusCode) { |
829 | m_directoryNode->deleteLater(); |
830 | m_directoryNode = nullptr; |
831 | |
832 | if (path.size() != 1) { |
833 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid path size" ; |
834 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
835 | return; |
836 | } |
837 | |
838 | if (path[0].targetName().name() != QLatin1String("Directory" )) { |
839 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid resolve name" << path[0].targetName().name(); |
840 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
841 | return; |
842 | } |
843 | |
844 | if (statusCode != QOpcUa::Good) { |
845 | qCWarning(QT_OPCUA_GDSCLIENT) << "Resolving directory failed" << statusCode; |
846 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
847 | return; |
848 | } |
849 | |
850 | if (targets.size() != 1) { |
851 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid number of results" ; |
852 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
853 | return; |
854 | } |
855 | |
856 | if (!targets[0].isFullyResolved()) { |
857 | qCWarning(QT_OPCUA_GDSCLIENT) << "Directory not fully resolved" ; |
858 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
859 | return; |
860 | } |
861 | |
862 | m_directoryNode = m_client->node(expandedNodeId: targets[0].targetId()); |
863 | if (!m_directoryNode) { |
864 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid directory node" ; |
865 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
866 | return; |
867 | } |
868 | |
869 | QObject::connect(sender: m_directoryNode, SIGNAL(resolveBrowsePathFinished(QList<QOpcUaBrowsePathTarget>, QList<QOpcUaRelativePathElement>, QOpcUa::UaStatusCode)), |
870 | receiver: q, SLOT(_q_handleResolveBrowsePathFinished(QList<QOpcUaBrowsePathTarget>, QList<QOpcUaRelativePathElement>, QOpcUa::UaStatusCode))); |
871 | QObject::connect(sender: m_directoryNode, SIGNAL(methodCallFinished(QString, QVariant, QOpcUa::UaStatusCode)), |
872 | receiver: q, SLOT(_q_handleDirectoryNodeMethodCallFinished(QString, QVariant, QOpcUa::UaStatusCode))); |
873 | |
874 | qCDebug(QT_OPCUA_GDSCLIENT) << "Directory node resolved:" << m_directoryNode->nodeId(); |
875 | this->resolveMethodNodes(); |
876 | }); |
877 | |
878 | QOpcUaRelativePathElement pathElement(QOpcUaQualifiedName(m_gdsNamespaceIndex, QLatin1String("Directory" )), |
879 | QOpcUa::ReferenceTypeId::Organizes); |
880 | QList<QOpcUaRelativePathElement> browsePath { pathElement }; |
881 | |
882 | if (!m_directoryNode->resolveBrowsePath(path: browsePath)) { |
883 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to resolve directory node" ; |
884 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
885 | delete m_directoryNode; |
886 | m_directoryNode = nullptr; |
887 | return; |
888 | } |
889 | } |
890 | |
891 | void QOpcUaGdsClientPrivate::resolveMethodNodes() |
892 | { |
893 | // See OPC UA Specification 1.04 part 12 6.3.2 "Directory" |
894 | |
895 | QOpcUaRelativePathElement pathElement(QOpcUaQualifiedName(m_gdsNamespaceIndex, QLatin1String()), |
896 | QOpcUa::ReferenceTypeId::HasComponent); |
897 | QList<QOpcUaRelativePathElement> browsePath { pathElement }; |
898 | |
899 | |
900 | // Resolve all needed nodes from the directory |
901 | for (const auto &key : std::as_const(t: elementsToResolve)) { |
902 | if (!m_directoryNodes.value(key).isEmpty()) |
903 | continue; // Already resolved |
904 | |
905 | auto target = browsePath[0].targetName(); |
906 | target.setName(key); |
907 | browsePath[0].setTargetName(target); |
908 | |
909 | if (!m_directoryNode->resolveBrowsePath(path: browsePath)) { |
910 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not resolve Directory node" ; |
911 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
912 | return; |
913 | } |
914 | } |
915 | } |
916 | |
917 | void QOpcUaGdsClientPrivate::_q_handleResolveBrowsePathFinished(QList<QOpcUaBrowsePathTarget> targets, QList<QOpcUaRelativePathElement> path, QOpcUa::UaStatusCode statusCode) { |
918 | if (path.size() != 1) { |
919 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid path size" ; |
920 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
921 | return; |
922 | } |
923 | |
924 | if (m_directoryNodes.contains(key: path[0].targetName().name()) || !elementsToResolve.contains(str: path[0].targetName().name())) { |
925 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid resolve name" << path[0].targetName().name(); |
926 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
927 | return; |
928 | } |
929 | |
930 | if (statusCode != QOpcUa::Good) { |
931 | qCWarning(QT_OPCUA_GDSCLIENT) << "Resolving directory failed" << statusCode; |
932 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
933 | return; |
934 | } |
935 | |
936 | if (targets.size() != 1) { |
937 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid number of results" ; |
938 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
939 | return; |
940 | } |
941 | |
942 | if (!targets[0].isFullyResolved()) { |
943 | qCWarning(QT_OPCUA_GDSCLIENT) << "Directory not fully resolved" ; |
944 | setError(QOpcUaGdsClient::Error::DirectoryNodeNotFound); |
945 | return; |
946 | } |
947 | |
948 | m_directoryNodes[path[0].targetName().name()] = targets[0].targetId().nodeId(); |
949 | |
950 | if (elementsToResolve.size() == m_directoryNodes.size()) { |
951 | qCDebug(QT_OPCUA_GDSCLIENT) << "All symbols resolved" ; |
952 | |
953 | if (m_appRecord.applicationId().isEmpty() || m_appRecord.applicationId() == QLatin1String("ns=0;i=0" )) |
954 | this->findRegisteredApplication(); |
955 | else |
956 | this->getApplication(); |
957 | } |
958 | } |
959 | |
960 | void QOpcUaGdsClientPrivate::getApplication() |
961 | { |
962 | // See OPC UA Specification 1.04 part 12 6.3.9 "GetApplication" |
963 | |
964 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
965 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
966 | return; |
967 | } |
968 | |
969 | if (!m_directoryNode) { |
970 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
971 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
972 | return; |
973 | } |
974 | |
975 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("GetApplication" )], |
976 | args: QList<QPair<QVariant, QOpcUa::Types>> { qMakePair(value1: QVariant(m_appRecord.applicationId()), value2: QOpcUa::Types::NodeId) })) { |
977 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method GetApplication" ; |
978 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
979 | return; |
980 | } |
981 | } |
982 | |
983 | void QOpcUaGdsClientPrivate::handleGetApplicationFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
984 | { |
985 | if (statusCode == QOpcUa::BadNotFound) { |
986 | qCWarning(QT_OPCUA_GDSCLIENT) << "No application with ID" << m_appRecord.applicationId() << "registered" ; |
987 | // Clear application ID and register |
988 | m_appRecord.setApplicationId(QString()); |
989 | |
990 | // Remove invalid id from settings |
991 | QSettings settings; |
992 | settings.remove(key: QLatin1String("gds/applicationId" )); |
993 | settings.sync(); |
994 | |
995 | restartWithCredentials(); |
996 | return; |
997 | } |
998 | |
999 | if (statusCode != QOpcUa::Good) { |
1000 | qCWarning(QT_OPCUA_GDSCLIENT) << "Get application failed" << statusCode; |
1001 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1002 | return; |
1003 | } |
1004 | |
1005 | auto extensionObject = result.value<QOpcUaExtensionObject>(); |
1006 | if (!extensionObject.encodingTypeId().isEmpty()) { |
1007 | if (extensionObject.encodingTypeId() != QOpcUa::nodeIdFromInteger(ns: m_gdsNamespaceIndex, ApplicationRecordDataType_Encoding_DefaultBinary)) { |
1008 | qCWarning(QT_OPCUA_GDSCLIENT) << "Unexpected return type:" << extensionObject.encodingTypeId(); |
1009 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1010 | return; |
1011 | } |
1012 | |
1013 | auto buffer = extensionObject.encodedBody(); |
1014 | QOpcUaBinaryDataEncoding decoder(&buffer); |
1015 | bool ok; |
1016 | QOpcUaApplicationRecordDataType appRecord = decoder.decode<QOpcUaApplicationRecordDataType>(success&: ok); |
1017 | |
1018 | if (!ok) { |
1019 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to decode data" ; |
1020 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1021 | return; |
1022 | } |
1023 | |
1024 | if (m_appRecord.applicationId() != appRecord.applicationId()) |
1025 | qCWarning(QT_OPCUA_GDSCLIENT) << "Returned application record contains a different application ID" << m_appRecord.applicationId() << appRecord.applicationId(); |
1026 | |
1027 | // FIXME: To be removed |
1028 | appRecord.setApplicationId(m_appRecord.applicationId()); |
1029 | |
1030 | m_appRecord = appRecord; |
1031 | qCInfo(QT_OPCUA_GDSCLIENT) << "Reusing application ID" << m_appRecord.applicationId() << "which is already registered at the server" ; |
1032 | } |
1033 | getCertificateGroups(); |
1034 | } |
1035 | |
1036 | void QOpcUaGdsClientPrivate::findRegisteredApplication() |
1037 | { |
1038 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1039 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1040 | return; |
1041 | } |
1042 | |
1043 | if (!m_directoryNode) { |
1044 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
1045 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1046 | return; |
1047 | } |
1048 | |
1049 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("FindApplications" )], |
1050 | args: QList<QPair<QVariant, QOpcUa::Types>> { qMakePair(value1: QVariant(m_appRecord.applicationUri()), value2: QOpcUa::Types::String) })) { |
1051 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method" ; |
1052 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1053 | return; |
1054 | } |
1055 | } |
1056 | |
1057 | void QOpcUaGdsClientPrivate::handleFindApplicationsFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1058 | { |
1059 | if (statusCode != QOpcUa::Good) { |
1060 | qCWarning(QT_OPCUA_GDSCLIENT) << "Find application failed" << statusCode; |
1061 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1062 | return; |
1063 | } |
1064 | |
1065 | auto extensionObject = result.value<QOpcUaExtensionObject>(); |
1066 | |
1067 | if (extensionObject.encodingTypeId().isEmpty()) { |
1068 | qCInfo(QT_OPCUA_GDSCLIENT) << "No application with URI" << m_appIdentitiy.applicationUri() << "registered" ; |
1069 | this->registerApplication(); |
1070 | return; |
1071 | } |
1072 | |
1073 | if (extensionObject.encodingTypeId() != QOpcUa::nodeIdFromInteger(ns: m_gdsNamespaceIndex, ApplicationRecordDataType_Encoding_DefaultBinary)) { |
1074 | qCWarning(QT_OPCUA_GDSCLIENT) << "Unexpected return type:" << extensionObject.encodingTypeId(); |
1075 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1076 | return; |
1077 | } |
1078 | |
1079 | auto buffer = extensionObject.encodedBody(); |
1080 | QOpcUaBinaryDataEncoding decoder(&buffer); |
1081 | bool ok; |
1082 | QOpcUaApplicationRecordDataType appRecord = decoder.decode<QOpcUaApplicationRecordDataType>(success&: ok); |
1083 | |
1084 | if (!ok) { |
1085 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to decode data" ; |
1086 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1087 | return; |
1088 | } |
1089 | |
1090 | m_appRecord = appRecord; |
1091 | qCInfo(QT_OPCUA_GDSCLIENT) << "Reusing application ID" << appRecord.applicationId() << "registered for URI" << appRecord.applicationUri(); |
1092 | getCertificateGroups(); |
1093 | } |
1094 | |
1095 | void QOpcUaGdsClientPrivate::registerApplication() |
1096 | { |
1097 | if (!m_appRecord.applicationId().isEmpty() && m_appRecord.applicationId() != QLatin1String("ns=0;i=0" )) |
1098 | return; |
1099 | |
1100 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1101 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1102 | return; |
1103 | } |
1104 | |
1105 | if (!m_directoryNode) { |
1106 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
1107 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1108 | return; |
1109 | } |
1110 | |
1111 | QByteArray buffer; |
1112 | QOpcUaBinaryDataEncoding encoder(&buffer); |
1113 | if (!encoder.encode(src: m_appRecord)) { |
1114 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to encode body" ; |
1115 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1116 | return; |
1117 | } |
1118 | |
1119 | QOpcUaExtensionObject parameter; |
1120 | parameter.setBinaryEncodedBody(encodedBody: buffer, typeId: QOpcUa::nodeIdFromInteger(ns: m_gdsNamespaceIndex, ApplicationRecordDataType_Encoding_DefaultBinary)); |
1121 | |
1122 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("RegisterApplication" )], |
1123 | args: QList<QOpcUa::TypedVariant> { QOpcUa::TypedVariant(parameter, QOpcUa::ExtensionObject) })) { |
1124 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method RegisterApplication" ; |
1125 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1126 | } |
1127 | } |
1128 | |
1129 | void QOpcUaGdsClientPrivate::handleRegisterApplicationFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1130 | { |
1131 | if (statusCode != QOpcUa::Good) { |
1132 | qCDebug(QT_OPCUA_GDSCLIENT) << "Requesting user credentials" ; |
1133 | if (statusCode == QOpcUa::BadUserAccessDenied && m_client->authenticationInformation().authenticationType() == QOpcUaUserTokenPolicy::Anonymous) { |
1134 | restartWithCredentials(); |
1135 | return; |
1136 | } |
1137 | qCWarning(QT_OPCUA_GDSCLIENT) << "Register application failed with" << statusCode; |
1138 | setError(QOpcUaGdsClient::Error::FailedToRegisterApplication); |
1139 | return; |
1140 | } |
1141 | |
1142 | m_appRecord.setApplicationId(result.toString()); |
1143 | |
1144 | QSettings settings; |
1145 | settings.setValue(key: QLatin1String("gds/applicationId" ), value: m_appRecord.applicationId()); |
1146 | settings.sync(); |
1147 | |
1148 | qCInfo(QT_OPCUA_GDSCLIENT) << "Registered application with id" << m_appRecord.applicationId(); |
1149 | getCertificateGroups(); |
1150 | } |
1151 | |
1152 | void QOpcUaGdsClientPrivate::getCertificateGroups() |
1153 | { |
1154 | // OPC UA Specification Version 1.04 Part 4 Chapter 7.6.6 GetCertificateGroups |
1155 | |
1156 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1157 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1158 | return; |
1159 | } |
1160 | |
1161 | if (!m_directoryNode) { |
1162 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
1163 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1164 | return; |
1165 | } |
1166 | |
1167 | QList<QOpcUa::TypedVariant> arguments; |
1168 | arguments.push_back(t: QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId)); |
1169 | |
1170 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("GetCertificateGroups" )], args: arguments)) { |
1171 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call GetCertificateGroups" ; |
1172 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1173 | } |
1174 | } |
1175 | |
1176 | void QOpcUaGdsClientPrivate::handleGetCertificateGroupsFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1177 | { |
1178 | Q_Q(QOpcUaGdsClient); |
1179 | |
1180 | if (statusCode != QOpcUa::Good) { |
1181 | qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate groups failed" << statusCode; |
1182 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1183 | return; |
1184 | } |
1185 | |
1186 | m_certificateGroups = result.value<QStringList>(); |
1187 | qCDebug(QT_OPCUA_GDSCLIENT) << "Certificate groups:" << m_certificateGroups; |
1188 | |
1189 | emit q->certificateGroupsReceived(certificateGroups: m_certificateGroups); |
1190 | resolveCertificateTypes(); |
1191 | } |
1192 | |
1193 | void QOpcUaGdsClientPrivate::resolveCertificateTypes() |
1194 | { |
1195 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1196 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1197 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1198 | return; |
1199 | } |
1200 | |
1201 | if (m_certificateGroups.isEmpty()) { |
1202 | qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate groups is empty" ; |
1203 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1204 | return; |
1205 | } |
1206 | |
1207 | delete m_certificateGroupNode; |
1208 | m_certificateGroupNode = m_client->node(nodeId: m_certificateGroups[0]); |
1209 | |
1210 | if (!m_certificateGroupNode) { |
1211 | qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate node failed" ; |
1212 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1213 | return; |
1214 | } |
1215 | |
1216 | QObject::connect(sender: m_certificateGroupNode, signal: &QOpcUaNode::resolveBrowsePathFinished, slot: [this](QList<QOpcUaBrowsePathTarget> targets, |
1217 | QList<QOpcUaRelativePathElement> path, |
1218 | QOpcUa::UaStatusCode statusCode) { |
1219 | if (path.size() != 1) { |
1220 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid path size" ; |
1221 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1222 | return; |
1223 | } |
1224 | |
1225 | if (path[0].targetName().name() != QLatin1String("CertificateTypes" )) { |
1226 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid resolve name" << path[0].targetName().name(); |
1227 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1228 | return; |
1229 | } |
1230 | |
1231 | if (statusCode != QOpcUa::Good) { |
1232 | qCWarning(QT_OPCUA_GDSCLIENT) << "Resolving certificate types failed" << statusCode; |
1233 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1234 | return; |
1235 | } |
1236 | |
1237 | if (targets.size() != 1) { |
1238 | qCWarning(QT_OPCUA_GDSCLIENT) << "Invalid number of results" ; |
1239 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1240 | return; |
1241 | } |
1242 | |
1243 | if (!targets[0].isFullyResolved()) { |
1244 | qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate types not fully resolved" ; |
1245 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1246 | return; |
1247 | } |
1248 | |
1249 | m_certificateTypesNodeId = targets[0].targetId().nodeId(); |
1250 | this->getCertificateTypes(); |
1251 | }); |
1252 | |
1253 | QOpcUaRelativePathElement pathElement(QOpcUaQualifiedName(0, QLatin1String("CertificateTypes" )), |
1254 | QOpcUa::ReferenceTypeId::Unspecified); |
1255 | QList<QOpcUaRelativePathElement> browsePath { pathElement }; |
1256 | |
1257 | |
1258 | if (!m_certificateGroupNode->resolveBrowsePath(path: browsePath)) { |
1259 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not resolve certificate type" ; |
1260 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1261 | return; |
1262 | } |
1263 | } |
1264 | |
1265 | void QOpcUaGdsClientPrivate::getCertificateTypes() |
1266 | { |
1267 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1268 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1269 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1270 | return; |
1271 | } |
1272 | |
1273 | if (m_certificateTypesNodeId.isEmpty()) { |
1274 | qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate types node id is empty" ; |
1275 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1276 | return; |
1277 | } |
1278 | |
1279 | delete m_certificateTypesNode; |
1280 | m_certificateTypesNode = m_client->node(nodeId: m_certificateTypesNodeId); |
1281 | |
1282 | if (!m_certificateTypesNode) { |
1283 | qCWarning(QT_OPCUA_GDSCLIENT) << "Certificate types node failed" ; |
1284 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1285 | return; |
1286 | } |
1287 | |
1288 | QObject::connect(sender: m_certificateTypesNode, signal: &QOpcUaNode::attributeUpdated, slot: [this](QOpcUa::NodeAttribute attr, QVariant value) { |
1289 | if (attr == QOpcUa::NodeAttribute::Value) |
1290 | qCWarning(QT_OPCUA_GDSCLIENT) << "possible certificate types" << value; |
1291 | registrationDone(); |
1292 | }); |
1293 | |
1294 | if (!m_certificateTypesNode->readValueAttribute()) { |
1295 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not read certificate types" ; |
1296 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1297 | return; |
1298 | } |
1299 | } |
1300 | |
1301 | void QOpcUaGdsClientPrivate::registrationDone() |
1302 | { |
1303 | Q_Q(QOpcUaGdsClient); |
1304 | |
1305 | setState(QOpcUaGdsClient::State::ApplicationRegistered); |
1306 | emit q->applicationRegistered(); |
1307 | |
1308 | m_certificateCheckTimer->start(); |
1309 | _q_certificateCheckTimeout(); // Force a check now |
1310 | |
1311 | m_trustListUpdateTimer->start(); |
1312 | _q_updateTrustList(); // Force a check now |
1313 | } |
1314 | |
1315 | void QOpcUaGdsClientPrivate::getCertificateStatus() |
1316 | { |
1317 | // OPC UA Specification Version 1.04 Part 4 Chapter 7.6.8 GetCertificateStatus |
1318 | |
1319 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1320 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1321 | return; |
1322 | } |
1323 | |
1324 | if (!m_directoryNode) { |
1325 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
1326 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1327 | return; |
1328 | } |
1329 | |
1330 | if (m_certificateGroups.isEmpty()) { |
1331 | qCWarning(QT_OPCUA_GDSCLIENT) << "No certificate groups received" ; |
1332 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1333 | return; |
1334 | } |
1335 | |
1336 | QString certificateType = QLatin1String("ns=0;i=0" ); // null node |
1337 | |
1338 | QList<QOpcUa::TypedVariant> arguments; |
1339 | arguments.push_back(t: QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId)); |
1340 | // Let the server choose the certificate group id |
1341 | arguments.push_back(t: QOpcUa::TypedVariant(m_certificateGroups[0], QOpcUa::NodeId)); |
1342 | // Let the server choose the certificate type id |
1343 | arguments.push_back(t: QOpcUa::TypedVariant(certificateType, QOpcUa::NodeId)); |
1344 | |
1345 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("GetCertificateStatus" )], args: arguments)) { |
1346 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call GetCertificateStatus" ; |
1347 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1348 | } |
1349 | } |
1350 | |
1351 | void QOpcUaGdsClientPrivate::handleGetCertificateStatusFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1352 | { |
1353 | Q_Q(QOpcUaGdsClient); |
1354 | |
1355 | if (statusCode != QOpcUa::Good) { |
1356 | qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate status failed" << statusCode; |
1357 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1358 | return; |
1359 | } |
1360 | |
1361 | if (result.toBool()) { |
1362 | qInfo(catFunc: QT_OPCUA_GDSCLIENT) << "Certificate needs update" ; |
1363 | emit q->certificateUpdateRequired(); |
1364 | startCertificateRequest(); |
1365 | } |
1366 | } |
1367 | |
1368 | void QOpcUaGdsClientPrivate::startCertificateRequest() |
1369 | { |
1370 | // OPC UA Specification Version 1.04 Part 4 Chapter 7.6.3 StartSigningRequest |
1371 | |
1372 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1373 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1374 | return; |
1375 | } |
1376 | |
1377 | if (!m_directoryNode) { |
1378 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
1379 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1380 | return; |
1381 | } |
1382 | |
1383 | QOpcUaKeyPair keyPair; |
1384 | QFile keyFile(m_pkiConfig.privateKeyFile()); |
1385 | |
1386 | qCDebug(QT_OPCUA_GDSCLIENT) << "Using private key" << keyFile.fileName(); |
1387 | |
1388 | if (!keyFile.open(flags: QFile::ReadOnly)) { |
1389 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to open private key file" << keyFile.fileName() << "for reading:" << keyFile.errorString(); |
1390 | setError(QOpcUaGdsClient::Error::ConnectionError); |
1391 | return; |
1392 | } |
1393 | |
1394 | auto data = keyFile.readAll(); |
1395 | keyFile.close(); |
1396 | |
1397 | if (!keyPair.loadFromPemData(data)) { |
1398 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load private key" ; |
1399 | setError(QOpcUaGdsClient::Error::ConnectionError); |
1400 | return; |
1401 | } |
1402 | |
1403 | if (!keyPair.hasPrivateKey()) { |
1404 | qCWarning(QT_OPCUA_GDSCLIENT) << "Private key does not contain a private key" ; |
1405 | setError(QOpcUaGdsClient::Error::ConnectionError); |
1406 | return; |
1407 | } |
1408 | |
1409 | auto csr = createSigningRequest(); |
1410 | csr.setEncoding(QOpcUaX509CertificateSigningRequest::Encoding::DER); |
1411 | const auto csrData = csr.createRequest(privateKey: keyPair); |
1412 | |
1413 | |
1414 | QList<QOpcUa::TypedVariant> arguments; |
1415 | arguments.push_back(t: QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId)); |
1416 | // Let the server choose the certificate group id |
1417 | arguments.push_back(t: QOpcUa::TypedVariant(QLatin1String("ns=0;i=0" ), QOpcUa::NodeId)); |
1418 | // Let the server choose the certificate type id |
1419 | arguments.push_back(t: QOpcUa::TypedVariant(QLatin1String("ns=0;i=0" ), QOpcUa::NodeId)); |
1420 | |
1421 | arguments.push_back(t: QOpcUa::TypedVariant(csrData, QOpcUa::ByteString)); |
1422 | |
1423 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("StartSigningRequest" )], args: arguments)) { |
1424 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call StartSigningRequest" ; |
1425 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1426 | } |
1427 | } |
1428 | |
1429 | void QOpcUaGdsClientPrivate::handleStartSigningRequestFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1430 | { |
1431 | if (statusCode != QOpcUa::Good) { |
1432 | qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate failed" << statusCode; |
1433 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1434 | return; |
1435 | } |
1436 | |
1437 | m_certificateRequestId = result.toString(); |
1438 | |
1439 | if (!m_certificateFinishTimer) |
1440 | m_certificateFinishTimer = new QTimer; |
1441 | |
1442 | m_certificateFinishTimer->setInterval(2000); |
1443 | m_certificateFinishTimer->setSingleShot(true); |
1444 | QObject::connect(sender: m_certificateFinishTimer, signal: &QTimer::timeout, slot: [this]() { |
1445 | this->finishCertificateRequest(); |
1446 | }); |
1447 | |
1448 | m_certificateFinishTimer->start(); |
1449 | } |
1450 | |
1451 | void QOpcUaGdsClientPrivate::finishCertificateRequest() |
1452 | { |
1453 | // OPC UA Specification Version 1.04 Part 4 Chapter 7.6.5 FinishRequest |
1454 | |
1455 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1456 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1457 | return; |
1458 | } |
1459 | |
1460 | if (m_certificateRequestId.isEmpty()) { |
1461 | qCWarning(QT_OPCUA_GDSCLIENT) << "No certificate request id" ; |
1462 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1463 | return; |
1464 | } |
1465 | |
1466 | QList<QOpcUa::TypedVariant> arguments; |
1467 | arguments.push_back(t: QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId)); |
1468 | arguments.push_back(t: QOpcUa::TypedVariant(m_certificateRequestId, QOpcUa::NodeId)); |
1469 | |
1470 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("FinishRequest" )], args: arguments)) { |
1471 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call FinishRequest" ; |
1472 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1473 | } |
1474 | } |
1475 | |
1476 | void QOpcUaGdsClientPrivate::handleFinishRequestFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1477 | { |
1478 | Q_Q(QOpcUaGdsClient); |
1479 | |
1480 | if (statusCode == QOpcUa::BadNothingToDo) { |
1481 | // Server not finished yet: Try again later |
1482 | m_certificateFinishTimer->setInterval(m_certificateFinishTimer->interval() * 2); |
1483 | m_certificateFinishTimer->start(); |
1484 | qCWarning(QT_OPCUA_GDSCLIENT) << "Server not finished yet: Trying again in" << m_certificateFinishTimer->interval() / 1000 << "s" ; |
1485 | return; |
1486 | } |
1487 | if (statusCode != QOpcUa::Good) { |
1488 | qCWarning(QT_OPCUA_GDSCLIENT) << "Getting certificate failed" << statusCode; |
1489 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1490 | return; |
1491 | } |
1492 | |
1493 | m_certificateRequestId.clear(); |
1494 | const auto resultList = result.toList(); |
1495 | |
1496 | if (resultList.size() != 3) { |
1497 | qCWarning(QT_OPCUA_GDSCLIENT) << "Expected list of 3 results but got" << resultList.size(); |
1498 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1499 | return; |
1500 | } |
1501 | |
1502 | const auto certificate = resultList[0].toByteArray(); |
1503 | //const auto privateKey = resultList[1].toByteArray(); |
1504 | const auto issuerList = resultList[2].toByteArray(); |
1505 | |
1506 | if (certificate.isEmpty() || issuerList.isEmpty()) { |
1507 | qCWarning(QT_OPCUA_GDSCLIENT) << "Certificates are empty" ; |
1508 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1509 | return; |
1510 | } |
1511 | |
1512 | qCInfo(QT_OPCUA_GDSCLIENT) << "Received new certificate" << certificate; |
1513 | |
1514 | QFile certificateFile(m_pkiConfig.clientCertificateFile()); |
1515 | if (!certificateFile.open(flags: QFile::WriteOnly)) { |
1516 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store certificate" ; |
1517 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1518 | return; |
1519 | } |
1520 | |
1521 | if (certificateFile.write(data: certificate) != certificate.size()) { |
1522 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store certificate data" ; |
1523 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1524 | return; |
1525 | } |
1526 | |
1527 | certificateFile.close(); |
1528 | |
1529 | // FIMXE: How to store this? |
1530 | QTemporaryFile issuerFile(m_pkiConfig.issuerListDirectory() + QLatin1String("/XXXXXX.der" )); |
1531 | if (!issuerFile.open()) { |
1532 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store issuer certificates to" << m_pkiConfig.issuerListDirectory(); |
1533 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1534 | return; |
1535 | } |
1536 | |
1537 | if (issuerFile.write(data: issuerList) != issuerList.size()) { |
1538 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not store certificate data" ; |
1539 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1540 | return; |
1541 | } |
1542 | |
1543 | qCDebug(QT_OPCUA_GDSCLIENT) << "issuer list stored to" << issuerFile.fileName(); |
1544 | issuerFile.close(); |
1545 | issuerFile.setAutoRemove(false); |
1546 | |
1547 | emit q->certificateUpdated(); |
1548 | } |
1549 | |
1550 | void QOpcUaGdsClientPrivate::unregisterApplication() |
1551 | { |
1552 | Q_Q(QOpcUaGdsClient); |
1553 | |
1554 | if (m_appRecord.applicationId().isEmpty() || m_appRecord.applicationId() == QLatin1String("ns=0;i=0" )) { |
1555 | emit q->unregistered(); |
1556 | return; |
1557 | } |
1558 | |
1559 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1560 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1561 | setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication); |
1562 | return; |
1563 | } |
1564 | |
1565 | if (!m_directoryNode) { |
1566 | qCWarning(QT_OPCUA_GDSCLIENT) << "No directory node" ; |
1567 | setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication); |
1568 | return; |
1569 | } |
1570 | |
1571 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("UnregisterApplication" )], |
1572 | args: QList<QOpcUa::TypedVariant> { QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId) })) { |
1573 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call method UnregisterApplication" ; |
1574 | setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication); |
1575 | } |
1576 | } |
1577 | |
1578 | void QOpcUaGdsClientPrivate::handleUnregisterApplicationFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1579 | { |
1580 | Q_Q(QOpcUaGdsClient); |
1581 | Q_UNUSED(result); |
1582 | |
1583 | if (statusCode != QOpcUa::Good) { |
1584 | qCWarning(QT_OPCUA_GDSCLIENT) << "Unregister application failed" << statusCode; |
1585 | setError(QOpcUaGdsClient::Error::FailedToUnregisterApplication); |
1586 | return; |
1587 | } |
1588 | |
1589 | m_client->disconnectFromEndpoint(); |
1590 | emit q->unregistered(); |
1591 | } |
1592 | |
1593 | void QOpcUaGdsClientPrivate::setPkiConfiguration(const QOpcUaPkiConfiguration &pkiConfig) |
1594 | { |
1595 | m_pkiConfig = pkiConfig; |
1596 | } |
1597 | |
1598 | const QOpcUaPkiConfiguration &QOpcUaGdsClientPrivate::pkiConfiguration() const |
1599 | { |
1600 | return m_pkiConfig; |
1601 | } |
1602 | |
1603 | void QOpcUaGdsClientPrivate::setApplicationIdentity(const QOpcUaApplicationIdentity &appIdentity) |
1604 | { |
1605 | m_appIdentitiy = appIdentity; |
1606 | } |
1607 | |
1608 | const QOpcUaApplicationIdentity &QOpcUaGdsClientPrivate::applicationIdentity() const |
1609 | { |
1610 | return m_appIdentitiy; |
1611 | } |
1612 | |
1613 | void QOpcUaGdsClientPrivate::setApplicationRecord(const QOpcUaApplicationRecordDataType &appRecord) |
1614 | { |
1615 | m_appRecord = appRecord; |
1616 | } |
1617 | |
1618 | const QOpcUaApplicationRecordDataType &QOpcUaGdsClientPrivate::applicationRecord() const |
1619 | { |
1620 | return m_appRecord; |
1621 | } |
1622 | |
1623 | void QOpcUaGdsClientPrivate::setCertificateSigningRequestPresets(const QOpcUaX509DistinguishedName &dn, const QString &dns) |
1624 | { |
1625 | csrPresets.dn = dn; |
1626 | csrPresets.dns = dns; |
1627 | } |
1628 | |
1629 | const QOpcUaX509DistinguishedName &QOpcUaGdsClientPrivate::distinguishedNameCertificateSigningRequestPreset() const |
1630 | { |
1631 | return csrPresets.dn; |
1632 | } |
1633 | |
1634 | const QString &QOpcUaGdsClientPrivate::dnsCertificateSigningRequestPreset() const |
1635 | { |
1636 | return csrPresets.dns; |
1637 | } |
1638 | |
1639 | void QOpcUaGdsClientPrivate::setCertificateCheckInterval(int interval) |
1640 | { |
1641 | m_certificateCheckTimer->setInterval(interval); |
1642 | } |
1643 | |
1644 | int QOpcUaGdsClientPrivate::certificateCheckInterval() const |
1645 | { |
1646 | return m_certificateCheckTimer->interval(); |
1647 | } |
1648 | |
1649 | void QOpcUaGdsClientPrivate::setTrustListUpdateInterval(int interval) |
1650 | { |
1651 | m_trustListUpdateTimer->setInterval(interval); |
1652 | } |
1653 | |
1654 | int QOpcUaGdsClientPrivate::trustListUpdateInterval() const |
1655 | { |
1656 | return m_trustListUpdateTimer->interval(); |
1657 | } |
1658 | |
1659 | QOpcUaGdsClient::Error QOpcUaGdsClientPrivate::error() const |
1660 | { |
1661 | return m_error; |
1662 | } |
1663 | |
1664 | QOpcUaGdsClient::State QOpcUaGdsClientPrivate::state() const |
1665 | { |
1666 | return m_state; |
1667 | } |
1668 | |
1669 | QOpcUaX509CertificateSigningRequest QOpcUaGdsClientPrivate::createSigningRequest() const |
1670 | { |
1671 | QOpcUaX509CertificateSigningRequest csr; |
1672 | QOpcUaX509DistinguishedName dn = csrPresets.dn; |
1673 | // Overwrite the application name because it has to match the identity |
1674 | dn.setEntry(type: QOpcUaX509DistinguishedName::Type::CommonName, value: m_appIdentitiy.applicationName()); |
1675 | csr.setSubject(dn); |
1676 | |
1677 | QOpcUaX509ExtensionSubjectAlternativeName *san = new QOpcUaX509ExtensionSubjectAlternativeName; |
1678 | san->addEntry(type: QOpcUaX509ExtensionSubjectAlternativeName::Type::URI, value: m_appIdentitiy.applicationUri()); |
1679 | if (!csrPresets.dns.isEmpty()) |
1680 | san->addEntry(type: QOpcUaX509ExtensionSubjectAlternativeName::Type::DNS, value: csrPresets.dns); |
1681 | san->setCritical(true); |
1682 | csr.addExtension(extension: san); |
1683 | |
1684 | QOpcUaX509ExtensionBasicConstraints *bc = new QOpcUaX509ExtensionBasicConstraints; |
1685 | bc->setCa(false); |
1686 | bc->setCritical(true); |
1687 | csr.addExtension(extension: bc); |
1688 | |
1689 | QOpcUaX509ExtensionKeyUsage *ku = new QOpcUaX509ExtensionKeyUsage; |
1690 | ku->setCritical(true); |
1691 | ku->setKeyUsage(keyUsage: QOpcUaX509ExtensionKeyUsage::KeyUsage::DigitalSignature); |
1692 | ku->setKeyUsage(keyUsage: QOpcUaX509ExtensionKeyUsage::KeyUsage::NonRepudiation); |
1693 | ku->setKeyUsage(keyUsage: QOpcUaX509ExtensionKeyUsage::KeyUsage::KeyEncipherment); |
1694 | ku->setKeyUsage(keyUsage: QOpcUaX509ExtensionKeyUsage::KeyUsage::DataEncipherment); |
1695 | ku->setKeyUsage(keyUsage: QOpcUaX509ExtensionKeyUsage::KeyUsage::CertificateSigning); |
1696 | csr.addExtension(extension: ku); |
1697 | |
1698 | return csr; |
1699 | } |
1700 | |
1701 | void QOpcUaGdsClientPrivate::_q_handleDirectoryNodeMethodCallFinished(QString methodNodeId, QVariant result, QOpcUa::UaStatusCode statusCode) |
1702 | { |
1703 | if (methodNodeId == m_directoryNodes[QLatin1String("UnregisterApplication" )]) { |
1704 | this->handleUnregisterApplicationFinished(result, statusCode); |
1705 | } else if ( methodNodeId == m_directoryNodes[QLatin1String("FinishRequest" )]) { |
1706 | this->handleFinishRequestFinished(result, statusCode); |
1707 | } else if (methodNodeId == m_directoryNodes[QLatin1String("StartSigningRequest" )]) { |
1708 | this->handleStartSigningRequestFinished(result, statusCode); |
1709 | } else if (methodNodeId == m_directoryNodes[QLatin1String("GetCertificateStatus" )]) { |
1710 | this->handleGetCertificateStatusFinished(result, statusCode); |
1711 | } else if (methodNodeId == m_directoryNodes[QLatin1String("GetCertificateGroups" )]) { |
1712 | this->handleGetCertificateGroupsFinished(result, statusCode); |
1713 | } else if (methodNodeId == m_directoryNodes[QLatin1String("RegisterApplication" )]) { |
1714 | this->handleRegisterApplicationFinished(result, statusCode); |
1715 | } else if (methodNodeId == m_directoryNodes[QLatin1String("FindApplications" )]) { |
1716 | this->handleFindApplicationsFinished(result, statusCode); |
1717 | } else if (methodNodeId == m_directoryNodes[QLatin1String("GetApplication" )]) { |
1718 | this->handleGetApplicationFinished(result, statusCode); |
1719 | } else if (methodNodeId == m_directoryNodes[QLatin1String("GetTrustList" )]) { |
1720 | this->handleGetTrustListFinished(result, statusCode); |
1721 | } else { |
1722 | qCWarning(QT_OPCUA_GDSCLIENT) << "Result unexpeced method call received" << methodNodeId; |
1723 | } |
1724 | } |
1725 | |
1726 | void QOpcUaGdsClientPrivate::_q_certificateCheckTimeout() |
1727 | { |
1728 | Q_Q(QOpcUaGdsClient); |
1729 | |
1730 | QFile file(m_pkiConfig.clientCertificateFile()); |
1731 | if (!file.open(flags: QFile::ReadOnly)) { |
1732 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load certificate from file" << m_pkiConfig.clientCertificateFile(); |
1733 | } |
1734 | |
1735 | QSslCertificate cert(&file, QSsl::Der); |
1736 | if (cert.isNull()) { |
1737 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to load certificate from file" << m_pkiConfig.clientCertificateFile(); |
1738 | return; |
1739 | } |
1740 | |
1741 | bool needsUpdate = false; |
1742 | |
1743 | if (cert.isSelfSigned()) { |
1744 | needsUpdate = true; |
1745 | qCInfo(QT_OPCUA_GDSCLIENT) << "Certificate is self-signed: requesting update" ; |
1746 | } |
1747 | |
1748 | if (QDateTime::currentDateTime().addSecs(secs: 24*60*60) > cert.expiryDate()) { |
1749 | needsUpdate = true; |
1750 | qCInfo(QT_OPCUA_GDSCLIENT) << "Certificate expiry date is due: requesting update" ; |
1751 | } |
1752 | |
1753 | if (!needsUpdate) |
1754 | getCertificateStatus(); |
1755 | |
1756 | if (needsUpdate) { |
1757 | emit q->certificateUpdateRequired(); |
1758 | startCertificateRequest(); |
1759 | } |
1760 | } |
1761 | |
1762 | void QOpcUaGdsClientPrivate::_q_updateTrustList() |
1763 | { |
1764 | // OPC UA Specification Version 1.04 Part 4 Chapter 7.6.7 GetTrustList |
1765 | |
1766 | if (!m_client || m_client->state() != QOpcUaClient::Connected) { |
1767 | qCWarning(QT_OPCUA_GDSCLIENT) << "No connection" ; |
1768 | return; |
1769 | } |
1770 | |
1771 | if (!m_certificateGroupNode) { |
1772 | qCWarning(QT_OPCUA_GDSCLIENT) << "No certificate group node" ; |
1773 | return; |
1774 | } |
1775 | |
1776 | QList<QOpcUa::TypedVariant> arguments; |
1777 | arguments.push_back(t: QOpcUa::TypedVariant(m_appRecord.applicationId(), QOpcUa::NodeId)); |
1778 | arguments.push_back(t: QOpcUa::TypedVariant(m_certificateGroupNode->nodeId(), QOpcUa::NodeId)); |
1779 | |
1780 | if (!m_directoryNode->callMethod(methodNodeId: m_directoryNodes[QLatin1String("GetTrustList" )], args: arguments)) { |
1781 | qCWarning(QT_OPCUA_GDSCLIENT) << "Failed to call GetTrustList" ; |
1782 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1783 | } |
1784 | } |
1785 | |
1786 | void QOpcUaGdsClientPrivate::handleGetTrustListFinished(const QVariant &result, QOpcUa::UaStatusCode statusCode) |
1787 | { |
1788 | Q_Q(QOpcUaGdsClient); |
1789 | |
1790 | if (statusCode != QOpcUa::Good) { |
1791 | qCWarning(QT_OPCUA_GDSCLIENT) << "Getting trust list node failed" << statusCode; |
1792 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1793 | return; |
1794 | } |
1795 | |
1796 | const auto trustListNodeId = result.toString(); |
1797 | |
1798 | if (trustListNodeId.isEmpty()) { |
1799 | qCWarning(QT_OPCUA_GDSCLIENT) << "Trust list node id is empty" ; |
1800 | setError(QOpcUaGdsClient::Error::FailedToGetCertificate); |
1801 | return; |
1802 | } |
1803 | |
1804 | delete m_trustListNode; |
1805 | m_trustListNode = m_client->node(nodeId: trustListNodeId); |
1806 | |
1807 | if (!m_trustListNode) { |
1808 | qCWarning(QT_OPCUA_GDSCLIENT) << "Trust list node failed" ; |
1809 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1810 | return; |
1811 | } |
1812 | |
1813 | QObject::connect(sender: m_trustListNode, signal: &QOpcUaNode::attributeUpdated, slot: [q](QOpcUa::NodeAttribute attr, QVariant value) { |
1814 | Q_UNUSED(value); |
1815 | if (attr == QOpcUa::NodeAttribute::Value) |
1816 | emit q->trustListUpdated(); |
1817 | }); |
1818 | |
1819 | if (!m_trustListNode->readValueAttribute()) { |
1820 | qCWarning(QT_OPCUA_GDSCLIENT) << "Could not read trust list" ; |
1821 | setError(QOpcUaGdsClient::Error::FailedToGetCertificateStatus); |
1822 | return; |
1823 | } |
1824 | } |
1825 | |
1826 | // When it is detected that authentication credentials are required this function |
1827 | // is used to re-connect to the server after requesting credentials |
1828 | void QOpcUaGdsClientPrivate::restartWithCredentials() |
1829 | { |
1830 | Q_Q(QOpcUaGdsClient); |
1831 | |
1832 | QOpcUaAuthenticationInformation authInfo; |
1833 | emit q->authenticationRequired(authInfo); |
1834 | m_client->setAuthenticationInformation(authInfo); |
1835 | |
1836 | qCInfo(QT_OPCUA_GDSCLIENT) << "Restarting connection with credentials" ; |
1837 | m_restartRequired = true; |
1838 | m_client->disconnectFromEndpoint(); |
1839 | } |
1840 | |
1841 | QT_END_NAMESPACE |
1842 | |
1843 | #include "moc_qopcuagdsclient.cpp" |
1844 | |