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