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