QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCNetworkHelper.cc
Go to the documentation of this file.
1#include "QGCNetworkHelper.h"
2
3#include <QtBluetooth/QBluetoothLocalDevice>
4#include <QtCore/QFile>
5#include <QtCore/QIODevice>
6#include <QtCore/QJsonDocument>
7#include <QtCore/QUrlQuery>
8#include <QtNetwork/QHostAddress>
9#include <QtNetwork/QHttpHeaders>
10#include <QtNetwork/QHttpPart>
11#include <QtNetwork/QNetworkAccessManager>
12#include <QtNetwork/QNetworkInformation>
13#include <QtNetwork/QNetworkProxyFactory>
14#include <QtNetwork/QSslSocket>
15#include <chrono>
16
17#include "QGCCompression.h"
18#include "QGCLoggingCategory.h"
19
20QGC_LOGGING_CATEGORY(QGCNetworkHelperLog, "Utilities.QGCNetworkHelper")
21
22namespace QGCNetworkHelper {
23
24// ============================================================================
25// HTTP Status Code Helpers
26// ============================================================================
27
29{
30 const auto code = static_cast<HttpStatusCode>(statusCode);
31 if (code >= HttpStatusCode::Continue && code < HttpStatusCode::Ok) {
32 return HttpStatusClass::Informational;
33 }
34 if (code >= HttpStatusCode::Ok && code < HttpStatusCode::MultipleChoices) {
35 return HttpStatusClass::Success;
36 }
37 if (code >= HttpStatusCode::MultipleChoices && code < HttpStatusCode::BadRequest) {
38 return HttpStatusClass::Redirection;
39 }
40 if (code >= HttpStatusCode::BadRequest && code < HttpStatusCode::InternalServerError) {
41 return HttpStatusClass::ClientError;
42 }
43 if (code >= HttpStatusCode::InternalServerError && code <= HttpStatusCode::NetworkConnectTimeoutError) {
44 return HttpStatusClass::ServerError;
45 }
46 return HttpStatusClass::Unknown;
47}
48
49QString httpStatusText(HttpStatusCode statusCode)
50{
51 switch (statusCode) {
52 // 1xx Informational
53 case HttpStatusCode::Continue:
54 return QStringLiteral("Continue");
55 case HttpStatusCode::SwitchingProtocols:
56 return QStringLiteral("Switching Protocols");
57 case HttpStatusCode::Processing:
58 return QStringLiteral("Processing");
59 // 2xx Success
60 case HttpStatusCode::Ok:
61 return QStringLiteral("OK");
62 case HttpStatusCode::Created:
63 return QStringLiteral("Created");
64 case HttpStatusCode::Accepted:
65 return QStringLiteral("Accepted");
66 case HttpStatusCode::NonAuthoritativeInformation:
67 return QStringLiteral("Non-Authoritative Information");
68 case HttpStatusCode::NoContent:
69 return QStringLiteral("No Content");
70 case HttpStatusCode::ResetContent:
71 return QStringLiteral("Reset Content");
72 case HttpStatusCode::PartialContent:
73 return QStringLiteral("Partial Content");
74 case HttpStatusCode::MultiStatus:
75 return QStringLiteral("Multi-Status");
76 case HttpStatusCode::AlreadyReported:
77 return QStringLiteral("Already Reported");
78 case HttpStatusCode::IMUsed:
79 return QStringLiteral("IM Used");
80 // 3xx Redirection
81 case HttpStatusCode::MultipleChoices:
82 return QStringLiteral("Multiple Choices");
83 case HttpStatusCode::MovedPermanently:
84 return QStringLiteral("Moved Permanently");
85 case HttpStatusCode::Found:
86 return QStringLiteral("Found");
87 case HttpStatusCode::SeeOther:
88 return QStringLiteral("See Other");
89 case HttpStatusCode::NotModified:
90 return QStringLiteral("Not Modified");
91 case HttpStatusCode::UseProxy:
92 return QStringLiteral("Use Proxy");
93 case HttpStatusCode::TemporaryRedirect:
94 return QStringLiteral("Temporary Redirect");
95 case HttpStatusCode::PermanentRedirect:
96 return QStringLiteral("Permanent Redirect");
97 // 4xx Client Errors
98 case HttpStatusCode::BadRequest:
99 return QStringLiteral("Bad Request");
100 case HttpStatusCode::Unauthorized:
101 return QStringLiteral("Unauthorized");
102 case HttpStatusCode::PaymentRequired:
103 return QStringLiteral("Payment Required");
104 case HttpStatusCode::Forbidden:
105 return QStringLiteral("Forbidden");
106 case HttpStatusCode::NotFound:
107 return QStringLiteral("Not Found");
108 case HttpStatusCode::MethodNotAllowed:
109 return QStringLiteral("Method Not Allowed");
110 case HttpStatusCode::NotAcceptable:
111 return QStringLiteral("Not Acceptable");
112 case HttpStatusCode::ProxyAuthenticationRequired:
113 return QStringLiteral("Proxy Authentication Required");
114 case HttpStatusCode::RequestTimeout:
115 return QStringLiteral("Request Timeout");
116 case HttpStatusCode::Conflict:
117 return QStringLiteral("Conflict");
118 case HttpStatusCode::Gone:
119 return QStringLiteral("Gone");
120 case HttpStatusCode::LengthRequired:
121 return QStringLiteral("Length Required");
122 case HttpStatusCode::PreconditionFailed:
123 return QStringLiteral("Precondition Failed");
124 case HttpStatusCode::PayloadTooLarge:
125 return QStringLiteral("Payload Too Large");
126 case HttpStatusCode::UriTooLong:
127 return QStringLiteral("URI Too Long");
128 case HttpStatusCode::UnsupportedMediaType:
129 return QStringLiteral("Unsupported Media Type");
130 case HttpStatusCode::RequestRangeNotSatisfiable:
131 return QStringLiteral("Range Not Satisfiable");
132 case HttpStatusCode::ExpectationFailed:
133 return QStringLiteral("Expectation Failed");
134 case HttpStatusCode::ImATeapot:
135 return QStringLiteral("I'm a teapot");
136 case HttpStatusCode::MisdirectedRequest:
137 return QStringLiteral("Misdirected Request");
138 case HttpStatusCode::UnprocessableEntity:
139 return QStringLiteral("Unprocessable Entity");
140 case HttpStatusCode::Locked:
141 return QStringLiteral("Locked");
142 case HttpStatusCode::FailedDependency:
143 return QStringLiteral("Failed Dependency");
144 case HttpStatusCode::UpgradeRequired:
145 return QStringLiteral("Upgrade Required");
146 case HttpStatusCode::PreconditionRequired:
147 return QStringLiteral("Precondition Required");
148 case HttpStatusCode::TooManyRequests:
149 return QStringLiteral("Too Many Requests");
150 case HttpStatusCode::RequestHeaderFieldsTooLarge:
151 return QStringLiteral("Request Header Fields Too Large");
152 case HttpStatusCode::UnavailableForLegalReasons:
153 return QStringLiteral("Unavailable For Legal Reasons");
154 // 5xx Server Errors
155 case HttpStatusCode::InternalServerError:
156 return QStringLiteral("Internal Server Error");
157 case HttpStatusCode::NotImplemented:
158 return QStringLiteral("Not Implemented");
159 case HttpStatusCode::BadGateway:
160 return QStringLiteral("Bad Gateway");
161 case HttpStatusCode::ServiceUnavailable:
162 return QStringLiteral("Service Unavailable");
163 case HttpStatusCode::GatewayTimeout:
164 return QStringLiteral("Gateway Timeout");
165 case HttpStatusCode::HttpVersionNotSupported:
166 return QStringLiteral("HTTP Version Not Supported");
167 case HttpStatusCode::VariantAlsoNegotiates:
168 return QStringLiteral("Variant Also Negotiates");
169 case HttpStatusCode::InsufficientStorage:
170 return QStringLiteral("Insufficient Storage");
171 case HttpStatusCode::LoopDetected:
172 return QStringLiteral("Loop Detected");
173 case HttpStatusCode::NotExtended:
174 return QStringLiteral("Not Extended");
175 case HttpStatusCode::NetworkAuthenticationRequired:
176 return QStringLiteral("Network Authentication Required");
177 case HttpStatusCode::NetworkConnectTimeoutError:
178 return QStringLiteral("Network Connect Timeout Error");
179 default:
180 return QStringLiteral("Unknown Status (%1)").arg(static_cast<int>(statusCode));
181 }
182}
183
184QString httpStatusText(int statusCode)
185{
186 return httpStatusText(static_cast<HttpStatusCode>(statusCode));
187}
188
189// ============================================================================
190// HTTP Methods
191// ============================================================================
192
194{
195 switch (method) {
196 case HttpMethod::Get:
197 return QStringLiteral("GET");
198 case HttpMethod::Post:
199 return QStringLiteral("POST");
200 case HttpMethod::Put:
201 return QStringLiteral("PUT");
202 case HttpMethod::Delete:
203 return QStringLiteral("DELETE");
204 case HttpMethod::Head:
205 return QStringLiteral("HEAD");
206 case HttpMethod::Options:
207 return QStringLiteral("OPTIONS");
208 case HttpMethod::Patch:
209 return QStringLiteral("PATCH");
210 case HttpMethod::Connect:
211 return QStringLiteral("CONNECT");
212 case HttpMethod::Trace:
213 return QStringLiteral("TRACE");
214 default:
215 return QStringLiteral("GET");
216 }
217}
218
219HttpMethod parseHttpMethod(const QString& methodStr)
220{
221 const QByteArray upper = methodStr.toUpper().toLatin1();
222 const char* str = upper.constData();
223
224 if (qstrcmp(str, "GET") == 0)
225 return HttpMethod::Get;
226 if (qstrcmp(str, "POST") == 0)
227 return HttpMethod::Post;
228 if (qstrcmp(str, "PUT") == 0)
229 return HttpMethod::Put;
230 if (qstrcmp(str, "DELETE") == 0)
231 return HttpMethod::Delete;
232 if (qstrcmp(str, "HEAD") == 0)
233 return HttpMethod::Head;
234 if (qstrcmp(str, "OPTIONS") == 0)
235 return HttpMethod::Options;
236 if (qstrcmp(str, "PATCH") == 0)
237 return HttpMethod::Patch;
238 if (qstrcmp(str, "CONNECT") == 0)
239 return HttpMethod::Connect;
240 if (qstrcmp(str, "TRACE") == 0)
241 return HttpMethod::Trace;
242
243 return HttpMethod::Get;
244}
245
246// ============================================================================
247// URL Utilities
248// ============================================================================
249
250bool isValidUrl(const QUrl& url)
251{
252 if (!url.isValid()) {
253 return false;
254 }
255
256 const QString scheme = url.scheme().toLower();
257 return scheme == QLatin1String("http") || scheme == QLatin1String("https") || scheme == QLatin1String("file") ||
258 scheme == QLatin1String("qrc") || scheme.isEmpty(); // Relative URL
259}
260
261bool isHttpUrl(const QUrl& url)
262{
263 const QString scheme = url.scheme().toLower();
264 return scheme == QLatin1String("http") || scheme == QLatin1String("https");
265}
266
267bool isHttpsUrl(const QUrl& url)
268{
269 return url.scheme().toLower() == QLatin1String("https");
270}
271
272QUrl normalizeUrl(const QUrl& url)
273{
274 if (!url.isValid()) {
275 return url;
276 }
277
278 QUrl normalized = url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash);
279 normalized.setScheme(normalized.scheme().toLower());
280 normalized.setHost(normalized.host().toLower());
281
282 const int port = normalized.port();
283 const QString scheme = normalized.scheme();
284 if ((scheme == QLatin1String("http") && port == 80) || (scheme == QLatin1String("https") && port == 443) ||
285 (scheme == QLatin1String("ftp") && port == 21)) {
286 normalized.setPort(-1);
287 }
288
289 return normalized;
290}
291
292QUrl ensureScheme(const QUrl& url, const QString& defaultScheme)
293{
294 if (!url.isValid()) {
295 return url;
296 }
297
298 if (url.scheme().isEmpty()) {
299 QUrl withScheme = url;
300 withScheme.setScheme(defaultScheme);
301 return withScheme;
302 }
303
304 return url;
305}
306
307QUrl buildUrl(const QString& baseUrl, const QMap<QString, QString>& params)
308{
309 QUrl url(baseUrl);
310 if (!url.isValid()) {
311 return url;
312 }
313
314 QUrlQuery query;
315 for (auto it = params.constBegin(); it != params.constEnd(); ++it) {
316 query.addQueryItem(it.key(), it.value());
317 }
318 url.setQuery(query);
319
320 return url;
321}
322
323QUrl buildUrl(const QString& baseUrl, const QList<QPair<QString, QString>>& params)
324{
325 QUrl url(baseUrl);
326 if (!url.isValid()) {
327 return url;
328 }
329
330 QUrlQuery query;
331 for (const auto& [key, value] : params) {
332 query.addQueryItem(key, value);
333 }
334 url.setQuery(query);
335
336 return url;
337}
338
339QUrl urlWithoutQuery(const QUrl& url)
340{
341 return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
342}
343
344namespace {
345enum class HostPortClassification
346{
347 NotHostPort,
348 Valid,
349 Invalid,
350};
351
352bool isAsciiAlphaNumeric(char character)
353{
354 return ((character >= 'a') && (character <= 'z')) || ((character >= 'A') && (character <= 'Z')) ||
355 ((character >= '0') && (character <= '9'));
356}
357
358bool isValidHostname(QString hostname)
359{
360 if (hostname.endsWith(QLatin1Char('.'))) {
361 hostname.chop(1);
362 }
363
364 const QByteArray aceHostname = QUrl::toAce(hostname);
365 if (aceHostname.isEmpty() || (aceHostname.size() > 253)) {
366 return false;
367 }
368
369 const QList<QByteArray> labels = aceHostname.split('.');
370 for (const QByteArray& label : labels) {
371 if (label.isEmpty() || (label.size() > 63) || !isAsciiAlphaNumeric(label.front()) ||
372 !isAsciiAlphaNumeric(label.back())) {
373 return false;
374 }
375 for (const char character : label) {
376 if (!isAsciiAlphaNumeric(character) && (character != '-')) {
377 return false;
378 }
379 }
380 }
381 return true;
382}
383
384HostPortClassification classifyHostPort(const QString& value)
385{
386 if (value.contains(QStringLiteral("://"))) {
387 return HostPortClassification::NotHostPort;
388 }
389
390 const qsizetype separator = value.lastIndexOf(QLatin1Char(':'));
391 if ((separator <= 0) || (separator == (value.size() - 1))) {
392 return HostPortClassification::NotHostPort;
393 }
394
395 const QString portText = value.sliced(separator + 1);
396 for (const QChar character : portText) {
397 if (!character.isDigit()) {
398 return HostPortClassification::NotHostPort;
399 }
400 }
401
402 bool portOk = false;
403 const int port = portText.toInt(&portOk);
404 if (!portOk || (port < 1) || (port > 65535)) {
405 return HostPortClassification::Invalid;
406 }
407
408 const QUrl authority(QStringLiteral("qgc://") + value, QUrl::StrictMode);
409 if (!authority.isValid() || authority.host().isEmpty() || !authority.userInfo().isEmpty() ||
410 !authority.path().isEmpty() || authority.hasQuery() || authority.hasFragment() ||
411 (authority.port(-1) != port)) {
412 return HostPortClassification::Invalid;
413 }
414
415 QHostAddress address;
416 const bool validHost = address.setAddress(authority.host()) || isValidHostname(authority.host());
417 return validHost ? HostPortClassification::Valid : HostPortClassification::Invalid;
418}
419} // namespace
420
421QString redactedUrlForLogging(const QUrl& url)
422{
423 if (url.isEmpty()) {
424 return QStringLiteral("<empty-url>");
425 }
426
427 const QString sourceText = url.isValid() ? url.toString(QUrl::FullyEncoded) : url.path();
428 const HostPortClassification hostPort = classifyHostPort(sourceText);
429 if (hostPort == HostPortClassification::Valid) {
430 return sourceText;
431 }
432 if (hostPort == HostPortClassification::Invalid) {
433 return QStringLiteral("<invalid-url length=%1>").arg(sourceText.size());
434 }
435 if (!url.isValid()) {
436 return QStringLiteral("<invalid-url>");
437 }
438
439 QUrl redactedUrl = url.adjusted(QUrl::RemoveUserInfo);
440 if (redactedUrl.hasQuery()) {
441 const auto queryItems = QUrlQuery(redactedUrl).queryItems(QUrl::FullyDecoded);
442 QUrlQuery redactedQuery;
443 for (const auto& queryItem : queryItems) {
444 redactedQuery.addQueryItem(queryItem.first, QStringLiteral("REDACTED"));
445 }
446 if (queryItems.isEmpty()) {
447 redactedUrl.setQuery(QString());
448 } else {
449 redactedUrl.setQuery(redactedQuery);
450 }
451 }
452 if (redactedUrl.hasFragment()) {
453 redactedUrl.setFragment(QStringLiteral("REDACTED"));
454 }
455
456 return redactedUrl.toDisplayString(QUrl::FullyEncoded);
457}
458
459QString redactedUrlForLogging(const QString& url)
460{
461 const HostPortClassification hostPort = classifyHostPort(url);
462 if (hostPort == HostPortClassification::Valid) {
463 return url;
464 }
465 if (hostPort == HostPortClassification::Invalid) {
466 return QStringLiteral("<invalid-url length=%1>").arg(url.size());
467 }
468 const QUrl parsedUrl(url);
469 if (!url.isEmpty() && !parsedUrl.isValid()) {
470 return QStringLiteral("<invalid-url length=%1>").arg(url.size());
471 }
472 return redactedUrlForLogging(parsedUrl);
473}
474
475// ============================================================================
476// Request Configuration
477// ============================================================================
478
479void configureRequest(QNetworkRequest& request, const RequestConfig& config)
480{
481 // Timeout
482 request.setTransferTimeout(config.timeoutMs);
483
484 // Redirect policy
485 if (config.allowRedirects) {
486 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
487 } else {
488 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
489 }
490
491 // HTTP/2
492 request.setAttribute(QNetworkRequest::Http2AllowedAttribute, config.http2Allowed);
493
494 // Caching
495 if (config.cacheEnabled) {
496 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
497 request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, true);
498 } else {
499 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
500 request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
501 }
502
503 // Background request
504 request.setAttribute(QNetworkRequest::BackgroundRequestAttribute, config.backgroundRequest);
505
506 using WK = QHttpHeaders::WellKnownHeader;
507 QHttpHeaders headers = request.headers();
508 headers.replaceOrAppend(WK::UserAgent, config.userAgent.isEmpty() ? defaultUserAgent() : config.userAgent);
509 if (!config.accept.isEmpty()) {
510 headers.replaceOrAppend(WK::Accept, config.accept);
511 }
512 if (!config.acceptEncoding.isEmpty()) {
513 headers.replaceOrAppend(WK::AcceptEncoding, config.acceptEncoding);
514 }
515 if (!config.contentType.isEmpty()) {
516 headers.replaceOrAppend(WK::ContentType, config.contentType);
517 }
518 headers.replaceOrAppend(WK::Connection, "keep-alive");
519 request.setHeaders(headers);
520
521 for (const auto& [attribute, value] : config.requestAttributes) {
522 request.setAttribute(attribute, value);
523 }
524
525 if (config.connectionCacheExpirySecs >= 0) {
526 request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute,
527 config.connectionCacheExpirySecs);
528 }
529 if (config.tcpKeepAliveIdleSecs >= 0) {
530 request.setTcpKeepAliveIdleTimeBeforeProbes(std::chrono::seconds(config.tcpKeepAliveIdleSecs));
531 }
532 if (config.tcpKeepAliveIntervalSecs >= 0) {
533 request.setTcpKeepAliveIntervalBetweenProbes(std::chrono::seconds(config.tcpKeepAliveIntervalSecs));
534 }
535 if (config.tcpKeepAliveProbeCount >= 0) {
536 request.setTcpKeepAliveProbeCount(config.tcpKeepAliveProbeCount);
537 }
538}
539
540QNetworkRequest createRequest(const QUrl& url, const RequestConfig& config)
541{
542 QNetworkRequest request(url);
543 configureRequest(request, config);
544 return request;
545}
546
547void setStandardHeaders(QNetworkRequest& request, const QString& userAgent)
548{
549 using WK = QHttpHeaders::WellKnownHeader;
550 QHttpHeaders headers = request.headers();
551 headers.replaceOrAppend(WK::UserAgent, userAgent.isEmpty() ? defaultUserAgent() : userAgent);
552 headers.replaceOrAppend(WK::Accept, "*/*");
553 headers.replaceOrAppend(WK::AcceptEncoding, "gzip, deflate");
554 headers.replaceOrAppend(WK::Connection, "keep-alive");
555 request.setHeaders(headers);
556}
557
558void setJsonHeaders(QNetworkRequest& request)
559{
560 using WK = QHttpHeaders::WellKnownHeader;
561 QHttpHeaders headers = request.headers();
562 headers.replaceOrAppend(WK::Accept, "application/json");
563 headers.replaceOrAppend(WK::ContentType, "application/json");
564 request.setHeaders(headers);
565}
566
567void setFormHeaders(QNetworkRequest& request)
568{
569 request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
570}
571
572// ============================================================================
573// Multipart Form Data Helpers
574// ============================================================================
575
576QHttpPart createFormField(const QString& name, const QString& value)
577{
578 QHttpPart part;
579 part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"%1\"").arg(name));
580 part.setBody(value.toUtf8());
581 return part;
582}
583
584QHttpPart createFilePart(const QString& name, const QString& fileName, const QString& contentType, QIODevice* device)
585{
586 QHttpPart part;
587 part.setHeader(QNetworkRequest::ContentTypeHeader, contentType);
588 part.setHeader(QNetworkRequest::ContentDispositionHeader,
589 QStringLiteral("form-data; name=\"%1\"; filename=\"%2\"").arg(name, fileName));
590 part.setBodyDevice(device);
591 return part;
592}
593
594QHttpPart createFilePart(const QString& name, const QString& fileName, QIODevice* device)
595{
596 return createFilePart(name, fileName, kContentTypeOctetStream, device);
597}
598
599// ============================================================================
600// SSL/TLS Configuration Builders
601// ============================================================================
602
603QSslConfiguration createInsecureSslConfig()
604{
605 QSslConfiguration config = QSslConfiguration::defaultConfiguration();
606 config.setPeerVerifyMode(QSslSocket::VerifyNone);
607 return config;
608}
609
610void applySslConfig(QNetworkRequest& request, const QSslConfiguration& config)
611{
612 request.setSslConfiguration(config);
613}
614
615QList<QSslCertificate> loadCaCertificates(const QString& filePath, QString* errorOut)
616{
617 const auto certs = QSslCertificate::fromPath(filePath, QSsl::Pem);
618 if (certs.isEmpty() && errorOut) {
619 *errorOut = QStringLiteral("No certificates found in %1").arg(filePath);
620 }
621 return certs;
622}
623
624bool loadClientCertAndKey(const QString& certPath, const QString& keyPath, QSslCertificate& certOut, QSslKey& keyOut,
625 QString* errorOut)
626{
627 const auto certs = QSslCertificate::fromPath(certPath, QSsl::Pem);
628 if (certs.isEmpty()) {
629 if (errorOut)
630 *errorOut = QStringLiteral("No certificate found in %1").arg(certPath);
631 return false;
632 }
633
634 QFile keyFile(keyPath);
635 if (!keyFile.open(QIODevice::ReadOnly)) {
636 if (errorOut)
637 *errorOut = QStringLiteral("Cannot open key file %1").arg(keyPath);
638 return false;
639 }
640
641 QSslKey key(&keyFile, QSsl::Rsa);
642 if (key.isNull()) {
643 keyFile.seek(0);
644 key = QSslKey(&keyFile, QSsl::Ec);
645 }
646 if (key.isNull()) {
647 if (errorOut)
648 *errorOut = QStringLiteral("Invalid key in %1").arg(keyPath);
649 return false;
650 }
651
652 certOut = certs.first();
653 keyOut = key;
654 return true;
655}
656
657// ============================================================================
658// JSON Response Helpers
659// ============================================================================
660
661QJsonDocument parseJson(const QByteArray& data, QJsonParseError* error)
662{
663 QJsonParseError localError;
664 QJsonParseError* errorPtr = (error != nullptr) ? error : &localError;
665
666 QJsonDocument doc = QJsonDocument::fromJson(data, errorPtr);
667
668 if (errorPtr->error != QJsonParseError::NoError) {
669 qCWarning(QGCNetworkHelperLog) << "JSON parse error:" << errorPtr->errorString() << "at offset"
670 << errorPtr->offset;
671 return {};
672 }
673
674 return doc;
675}
676
677QJsonDocument parseJsonReply(QNetworkReply* reply, QJsonParseError* error)
678{
679 if (reply == nullptr) {
680 if (error != nullptr) {
681 error->error = QJsonParseError::UnterminatedObject;
682 error->offset = 0;
683 }
684 return {};
685 }
686
687 if (reply->error() != QNetworkReply::NoError) {
688 qCWarning(QGCNetworkHelperLog) << "Network error before JSON parse:" << reply->errorString();
689 if (error != nullptr) {
690 error->error = QJsonParseError::UnterminatedObject;
691 error->offset = 0;
692 }
693 return {};
694 }
695
696 return parseJson(reply->readAll(), error);
697}
698
699bool looksLikeJson(const QByteArray& data)
700{
701 if (data.isEmpty()) {
702 return false;
703 }
704
705 // Skip leading whitespace
706 for (int i = 0; i < data.size(); ++i) {
707 const char c = data.at(i);
708 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
709 continue;
710 }
711 return c == '{' || c == '[';
712 }
713
714 return false;
715}
716
717// ============================================================================
718// Network Reply Helpers
719// ============================================================================
720
721int httpStatusCode(const QNetworkReply* reply)
722{
723 if (!reply) {
724 return -1;
725 }
726 const QVariant statusAttr = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
727 return statusAttr.isValid() ? statusAttr.toInt() : -1;
728}
729
730QUrl redirectUrl(const QNetworkReply* reply)
731{
732 if (!reply) {
733 return {};
734 }
735
736 const QVariant redirectAttr = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
737 if (redirectAttr.isNull()) {
738 return {};
739 }
740
741 const QUrl redirectTarget = redirectAttr.toUrl();
742 return reply->url().resolved(redirectTarget);
743}
744
745QString errorMessage(const QNetworkReply* reply)
746{
747 if (!reply) {
748 return QStringLiteral("No reply");
749 }
750
751 // Check for network error first
752 if (reply->error() != QNetworkReply::NoError) {
753 return reply->errorString();
754 }
755
756 // Check HTTP status code
757 const int status = httpStatusCode(reply);
758 if (status >= 400) {
759 return QStringLiteral("HTTP %1: %2").arg(status).arg(httpStatusText(status));
760 }
761
762 return {};
763}
764
765bool isSuccess(const QNetworkReply* reply)
766{
767 if (!reply) {
768 return false;
769 }
770
771 if (reply->error() != QNetworkReply::NoError) {
772 return false;
773 }
774
775 const int status = httpStatusCode(reply);
776 return status == -1 || isHttpSuccess(status); // -1 means non-HTTP (e.g., file://)
777}
778
779bool isRedirect(const QNetworkReply* reply)
780{
781 if (!reply) {
782 return false;
783 }
784
785 const int status = httpStatusCode(reply);
786 return isHttpRedirect(status) || !redirectUrl(reply).isEmpty();
787}
788
789QString contentType(const QNetworkReply* reply)
790{
791 if (!reply) {
792 return {};
793 }
794 return reply->header(QNetworkRequest::ContentTypeHeader).toString();
795}
796
797qint64 contentLength(const QNetworkReply* reply)
798{
799 if (!reply) {
800 return -1;
801 }
802 const QVariant lenAttr = reply->header(QNetworkRequest::ContentLengthHeader);
803 return lenAttr.isValid() ? lenAttr.toLongLong() : -1;
804}
805
806bool isJsonResponse(const QNetworkReply* reply)
807{
808 const QString type = contentType(reply);
809 return type.contains(QLatin1String("application/json"), Qt::CaseInsensitive) ||
810 type.contains(QLatin1String("+json"), Qt::CaseInsensitive);
811}
812
813// ============================================================================
814// Network Availability
815// ============================================================================
816
817QNetworkInformation* networkInformation()
818{
819 if (!QNetworkInformation::loadDefaultBackend()) {
820 qCDebug(QGCNetworkHelperLog) << "Failed to load network information backend";
821 }
822 if (!QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability)) {
823 qCDebug(QGCNetworkHelperLog) << "Network information backend does not provide reachability";
824 }
825 return QNetworkInformation::instance();
826}
827
829{
830 const QNetworkInformation* netInfo = networkInformation();
831 return !netInfo || netInfo->reachability() != QNetworkInformation::Reachability::Disconnected;
832}
833
835{
836 const QNetworkInformation* netInfo = networkInformation();
837 return netInfo && netInfo->reachability() == QNetworkInformation::Reachability::Online;
838}
839
841{
842 const QNetworkInformation* netInfo = networkInformation();
843 return netInfo && netInfo->transportMedium() == QNetworkInformation::TransportMedium::Ethernet;
844}
845
847{
848 const QList<QBluetoothHostInfo> devices = QBluetoothLocalDevice::allDevices();
849 return !devices.isEmpty();
850}
851
853{
854 const QNetworkInformation* netInfo = networkInformation();
855 if (!netInfo) {
856 return ConnectionType::Unknown;
857 }
858
859 if (netInfo->reachability() == QNetworkInformation::Reachability::Disconnected) {
860 return ConnectionType::None;
861 }
862
863 switch (netInfo->transportMedium()) {
864 case QNetworkInformation::TransportMedium::Ethernet:
865 return ConnectionType::Ethernet;
866 case QNetworkInformation::TransportMedium::WiFi:
867 return ConnectionType::WiFi;
868 case QNetworkInformation::TransportMedium::Cellular:
869 return ConnectionType::Cellular;
870 case QNetworkInformation::TransportMedium::Bluetooth:
871 return ConnectionType::Bluetooth;
872 default:
873 return ConnectionType::Unknown;
874 }
875}
876
878{
879 switch (type) {
880 case ConnectionType::None:
881 return QStringLiteral("None");
882 case ConnectionType::Ethernet:
883 return QStringLiteral("Ethernet");
884 case ConnectionType::WiFi:
885 return QStringLiteral("WiFi");
886 case ConnectionType::Cellular:
887 return QStringLiteral("Cellular");
888 case ConnectionType::Bluetooth:
889 return QStringLiteral("Bluetooth");
890 case ConnectionType::Unknown:
891 default:
892 return QStringLiteral("Unknown");
893 }
894}
895
896// ============================================================================
897// SSL/TLS Helpers
898// ============================================================================
899
900void ignoreSslErrors(QNetworkReply* reply)
901{
902 if (!reply) {
903 return;
904 }
905
906 QObject::connect(reply, &QNetworkReply::sslErrors, reply, [reply](const QList<QSslError>& errors) {
907 qCWarning(QGCNetworkHelperLog) << "Ignoring SSL errors for" << reply->url();
908 for (const QSslError& error : errors) {
909 qCDebug(QGCNetworkHelperLog) << " -" << error.errorString();
910 }
911 reply->ignoreSslErrors();
912 });
913}
914
915void ignoreSslErrorsIfNeeded(QNetworkReply* reply)
916{
917 if (!reply) {
918 return;
919 }
920
921 // Check for OpenSSL version mismatch: Qt built with 1.x but running with 3.x
922 const bool sslLibraryBuildIs1x = ((QSslSocket::sslLibraryBuildVersionNumber() & 0xf0000000) == 0x10000000);
923 const bool sslLibraryIs3x = ((QSslSocket::sslLibraryVersionNumber() & 0xf0000000) == 0x30000000);
924 if (sslLibraryBuildIs1x && sslLibraryIs3x) {
925 qCWarning(QGCNetworkHelperLog) << "Ignoring SSL certificate errors due to OpenSSL version mismatch";
926 QList<QSslError> errorsThatCanBeIgnored;
927 errorsThatCanBeIgnored << QSslError(QSslError::NoPeerCertificate);
928 reply->ignoreSslErrors(errorsThatCanBeIgnored);
929 }
930}
931
933{
934 return QSslSocket::supportsSsl();
935}
936
937QString sslVersion()
938{
939 return QSslSocket::sslLibraryVersionString();
940}
941
942// ============================================================================
943// Network Access Manager Helpers
944// ============================================================================
945
947{
948 QNetworkProxyFactory::setUseSystemConfiguration(true);
949}
950
951} // namespace QGCNetworkHelper
Config config
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
HttpStatusClass classifyHttpStatus(int statusCode)
Classify an HTTP status code.
bool isSuccess(const QNetworkReply *reply)
Check if reply indicates success (no error and HTTP 2xx)
QHttpServerRequest::Method HttpMethod
HTTP request methods - uses Qt's QHttpServerRequest::Method enum.
QNetworkInformation * networkInformation()
The platform's network information, preferring a backend that reports reachability; null without a ba...
HttpStatusClass
HTTP status code ranges.
void applySslConfig(QNetworkRequest &request, const QSslConfiguration &config)
QHttpServerResponder::StatusCode HttpStatusCode
HTTP status codes - uses Qt's QHttpServerResponder::StatusCode enum.
QList< QSslCertificate > loadCaCertificates(const QString &filePath, QString *errorOut)
bool looksLikeJson(const QByteArray &data)
bool isJsonResponse(const QNetworkReply *reply)
Check if response is JSON based on Content-Type.
const QString kContentTypeOctetStream
ConnectionType
Network connection types.
QNetworkRequest createRequest(const QUrl &url, const RequestConfig &config)
bool isRedirect(const QNetworkReply *reply)
Check if reply indicates a redirect.
bool isNetworkEthernet()
Check if current network connection is Ethernet.
QString sslVersion()
Get SSL library version string.
bool isHttpRedirect(int statusCode)
Check if HTTP status indicates redirect (3xx)
HttpMethod parseHttpMethod(const QString &methodStr)
bool isHttpUrl(const QUrl &url)
Check if URL uses HTTP or HTTPS scheme.
QHttpPart createFormField(const QString &name, const QString &value)
void setFormHeaders(QNetworkRequest &request)
Set form data content headers.
QUrl redirectUrl(const QNetworkReply *reply)
void ignoreSslErrorsIfNeeded(QNetworkReply *reply)
void setStandardHeaders(QNetworkRequest &request, const QString &userAgent)
Set standard browser-like headers on a request.
bool isBluetoothAvailable()
Check if Bluetooth is available on this device.
QString contentType(const QNetworkReply *reply)
Get Content-Type header from reply.
bool loadClientCertAndKey(const QString &certPath, const QString &keyPath, QSslCertificate &certOut, QSslKey &keyOut, QString *errorOut)
int httpStatusCode(const QNetworkReply *reply)
QString redactedUrlForLogging(const QUrl &url)
QSslConfiguration createInsecureSslConfig()
QString defaultUserAgent()
"<application>/<version> (Qt <version>)".
QUrl buildUrl(const QString &baseUrl, const QMap< QString, QString > &params)
Build URL with query parameters from a map.
bool isNetworkAvailable()
Check if network is available (not disconnected); assumed available without a backend.
QString httpStatusText(HttpStatusCode statusCode)
bool isHttpsUrl(const QUrl &url)
Check if URL uses secure HTTPS scheme.
QString httpMethodName(HttpMethod method)
Get string name for an HTTP method (e.g., "GET", "POST")
bool isHttpSuccess(int statusCode)
Check if HTTP status indicates success (2xx)
ConnectionType connectionType()
Get current network connection type.
QJsonDocument parseJson(const QByteArray &data, QJsonParseError *error)
bool isValidUrl(const QUrl &url)
QHttpPart createFilePart(const QString &name, const QString &fileName, const QString &contentType, QIODevice *device)
QJsonDocument parseJsonReply(QNetworkReply *reply, QJsonParseError *error)
QUrl normalizeUrl(const QUrl &url)
Normalize URL (lowercase scheme/host, remove default ports, trailing slashes)
QUrl urlWithoutQuery(const QUrl &url)
Get URL without query string and fragment.
void setJsonHeaders(QNetworkRequest &request)
Set JSON content headers (Accept and Content-Type)
qint64 contentLength(const QNetworkReply *reply)
Get Content-Length header from reply (-1 if not present)
QString connectionTypeName(ConnectionType type)
Get human-readable name for connection type.
QString errorMessage(const QNetworkReply *reply)
QUrl ensureScheme(const QUrl &url, const QString &defaultScheme)
Ensure URL has scheme, defaulting to https:// if missing.
bool isInternetAvailable()
Check if internet is reachable (online state, stricter than isNetworkAvailable)
void ignoreSslErrors(QNetworkReply *reply)
void configureRequest(QNetworkRequest &request, const RequestConfig &config)
bool isSslAvailable()
Check if SSL is available.
GPSCommandSequence port(QByteArrayView port, unsigned baud)
Clears the SBF output of port, keeps a serial port at the link's baud, and selects SBF and the WGS84 ...
Definition SBFPlan.cc:34
Common request configuration options.