Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Camera/VehicleCameraControl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1817,7 +1817,8 @@ void VehicleCameraControl::setCurrentStream(int stream)
if (stream != _currentStream && stream >= 0 && stream < _streamLabels.count()) {
QGCVideoStreamInfo* pInfo = currentStreamInstance();
if(pInfo) {
qCDebug(VehicleCameraControlLog) << "Stopping stream:" << pInfo->uri();
qCDebug(VehicleCameraControlLog)
<< "Stopping stream:" << QGCNetworkHelper::redactedUrlForLogging(pInfo->uri());
//-- Stop current stream
_vehicle->sendMavCommand(
_compID, // Target component
Expand All @@ -1829,7 +1830,8 @@ void VehicleCameraControl::setCurrentStream(int stream)
pInfo = currentStreamInstance();
if(pInfo) {
//-- Start new stream
qCDebug(VehicleCameraControlLog) << "Starting stream:" << pInfo->uri();
qCDebug(VehicleCameraControlLog)
<< "Starting stream:" << QGCNetworkHelper::redactedUrlForLogging(pInfo->uri());
_vehicle->sendMavCommand(
_compID, // Target component
MAV_CMD_VIDEO_START_STREAMING, // Command id
Expand Down
25 changes: 17 additions & 8 deletions src/Settings/VideoSettings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "VideoManager.h"

#include "QGCLoggingCategory.h"
#include "QGCNetworkHelper.h"
#include <QtCore/QSettings>
#include <QtCore/QVariantList>

Expand Down Expand Up @@ -243,23 +244,31 @@ bool VideoSettings::streamConfigured(void)
}
//-- If UDP, check for URL
if(vSource == videoSourceUDPH264 || vSource == videoSourceUDPH265) {
qCDebug(VideoSettingsLog) << "Testing configuration for UDP Stream:" << udpUrl()->rawValue().toString();
return !udpUrl()->rawValue().toString().isEmpty();
const QString url = udpUrl()->rawValue().toString();
qCDebug(VideoSettingsLog) << "Testing configuration for UDP Stream:"
<< QGCNetworkHelper::redactedUrlForLogging(url);
return !url.isEmpty();
}
//-- If RTSP, check for URL
if(vSource == videoSourceRTSP) {
qCDebug(VideoSettingsLog) << "Testing configuration for RTSP Stream:" << rtspUrl()->rawValue().toString();
return !rtspUrl()->rawValue().toString().isEmpty();
const QString url = rtspUrl()->rawValue().toString();
qCDebug(VideoSettingsLog) << "Testing configuration for RTSP Stream:"
<< QGCNetworkHelper::redactedUrlForLogging(url);
return !url.isEmpty();
}
//-- If TCP, check for URL
if(vSource == videoSourceTCP) {
qCDebug(VideoSettingsLog) << "Testing configuration for TCP Stream:" << tcpUrl()->rawValue().toString();
return !tcpUrl()->rawValue().toString().isEmpty();
const QString url = tcpUrl()->rawValue().toString();
qCDebug(VideoSettingsLog) << "Testing configuration for TCP Stream:"
<< QGCNetworkHelper::redactedUrlForLogging(url);
return !url.isEmpty();
}
//-- If MPEG-TS, check for URL
if(vSource == videoSourceMPEGTS) {
qCDebug(VideoSettingsLog) << "Testing configuration for MPEG-TS Stream:" << udpUrl()->rawValue().toString();
return !udpUrl()->rawValue().toString().isEmpty();
const QString url = udpUrl()->rawValue().toString();
qCDebug(VideoSettingsLog) << "Testing configuration for MPEG-TS Stream:"
<< QGCNetworkHelper::redactedUrlForLogging(url);
return !url.isEmpty();
}
//-- If Herelink Air unit, good to go
if(vSource == videoSourceHerelinkAirUnit) {
Expand Down
132 changes: 132 additions & 0 deletions src/Utilities/Network/QGCNetworkHelper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <QtCore/QIODevice>
#include <QtCore/QJsonDocument>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QHostAddress>
#include <QtNetwork/QHttpHeaders>
#include <QtNetwork/QHttpPart>
#include <QtNetwork/QNetworkAccessManager>
Expand Down Expand Up @@ -342,6 +343,137 @@ QUrl urlWithoutQuery(const QUrl& url)
return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
}

namespace {
enum class HostPortClassification
{
NotHostPort,
Valid,
Invalid,
};

bool isAsciiAlphaNumeric(char character)
{
return ((character >= 'a') && (character <= 'z')) || ((character >= 'A') && (character <= 'Z')) ||
((character >= '0') && (character <= '9'));
}

bool isValidHostname(QString hostname)
{
if (hostname.endsWith(QLatin1Char('.'))) {
hostname.chop(1);
}

const QByteArray aceHostname = QUrl::toAce(hostname);
if (aceHostname.isEmpty() || (aceHostname.size() > 253)) {
return false;
}

const QList<QByteArray> labels = aceHostname.split('.');
for (const QByteArray& label : labels) {
if (label.isEmpty() || (label.size() > 63) || !isAsciiAlphaNumeric(label.front()) ||
!isAsciiAlphaNumeric(label.back())) {
return false;
}
for (const char character : label) {
if (!isAsciiAlphaNumeric(character) && (character != '-')) {
return false;
}
}
}
return true;
}

HostPortClassification classifyHostPort(const QString& value)
{
if (value.contains(QStringLiteral("://"))) {
return HostPortClassification::NotHostPort;
}

const qsizetype separator = value.lastIndexOf(QLatin1Char(':'));
if ((separator <= 0) || (separator == (value.size() - 1))) {
return HostPortClassification::NotHostPort;
}

const QString portText = value.sliced(separator + 1);
for (const QChar character : portText) {
if (!character.isDigit()) {
return HostPortClassification::NotHostPort;
}
}

bool portOk = false;
const int port = portText.toInt(&portOk);
if (!portOk || (port < 1) || (port > 65535)) {
return HostPortClassification::Invalid;
}

const QUrl authority(QStringLiteral("qgc://") + value, QUrl::StrictMode);
if (!authority.isValid() || authority.host().isEmpty() || !authority.userInfo().isEmpty() ||
!authority.path().isEmpty() || authority.hasQuery() || authority.hasFragment() ||
(authority.port(-1) != port)) {
return HostPortClassification::Invalid;
}

QHostAddress address;
const bool validHost = address.setAddress(authority.host()) || isValidHostname(authority.host());
return validHost ? HostPortClassification::Valid : HostPortClassification::Invalid;
}
} // namespace

QString redactedUrlForLogging(const QUrl& url)
{
if (url.isEmpty()) {
return QStringLiteral("<empty-url>");
}

const QString sourceText = url.isValid() ? url.toString(QUrl::FullyEncoded) : url.path();
const HostPortClassification hostPort = classifyHostPort(sourceText);
if (hostPort == HostPortClassification::Valid) {
return sourceText;
}
if (hostPort == HostPortClassification::Invalid) {
return QStringLiteral("<invalid-url length=%1>").arg(sourceText.size());
}
if (!url.isValid()) {
return QStringLiteral("<invalid-url>");
}

QUrl redactedUrl = url.adjusted(QUrl::RemoveUserInfo);
if (redactedUrl.hasQuery()) {
const auto queryItems = QUrlQuery(redactedUrl).queryItems(QUrl::FullyDecoded);
QUrlQuery redactedQuery;
for (const auto& queryItem : queryItems) {
redactedQuery.addQueryItem(queryItem.first, QStringLiteral("REDACTED"));
}
if (queryItems.isEmpty()) {
redactedUrl.setQuery(QString());
} else {
redactedUrl.setQuery(redactedQuery);
}
}
if (redactedUrl.hasFragment()) {
redactedUrl.setFragment(QStringLiteral("REDACTED"));
}

return redactedUrl.toDisplayString(QUrl::FullyEncoded);
}

QString redactedUrlForLogging(const QString& url)
{
const HostPortClassification hostPort = classifyHostPort(url);
if (hostPort == HostPortClassification::Valid) {
return url;
}
if (hostPort == HostPortClassification::Invalid) {
return QStringLiteral("<invalid-url length=%1>").arg(url.size());
}
const QUrl parsedUrl(url);
if (!url.isEmpty() && !parsedUrl.isValid()) {
return QStringLiteral("<invalid-url length=%1>").arg(url.size());
}
return redactedUrlForLogging(parsedUrl);
}

// ============================================================================
// Request Configuration
// ============================================================================
Expand Down
5 changes: 5 additions & 0 deletions src/Utilities/Network/QGCNetworkHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ QUrl buildUrl(const QString& baseUrl, const QList<QPair<QString, QString>>& para
/// Get URL without query string and fragment
QUrl urlWithoutQuery(const QUrl& url);

/// Return a URL suitable for diagnostics. Stream identity is preserved while user info,
/// query values, and fragment content are redacted.
QString redactedUrlForLogging(const QUrl& url);
QString redactedUrlForLogging(const QString& url);

// ============================================================================
// Request Configuration
// ============================================================================
Expand Down
13 changes: 9 additions & 4 deletions src/VideoManager/VideoManager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "QGCCameraManager.h"
#include "QGCCorePlugin.h"
#include "QGCLoggingCategory.h"
#include "QGCNetworkHelper.h"
#include "QGCVideoStreamInfo.h"
#include "SettingsManager.h"
#include "SubtitleWriter.h"
Expand Down Expand Up @@ -593,7 +594,8 @@ bool VideoManager::_updateAutoStream(VideoReceiver *receiver)
return false;
}

qCDebug(VideoManagerLog) << QString("Configure stream (%1):").arg(receiver->name()) << pInfo->uri();
qCDebug(VideoManagerLog) << QString("Configure stream (%1):").arg(receiver->name())
<< QGCNetworkHelper::redactedUrlForLogging(pInfo->uri());

QString source, url;
switch (pInfo->type()) {
Expand Down Expand Up @@ -651,7 +653,7 @@ bool VideoManager::_updateVideoUri(VideoReceiver *receiver, const QString &uri)
return false;
}

qCDebug(VideoManagerLog) << "New Video URI" << uri;
qCDebug(VideoManagerLog) << "New Video URI" << QGCNetworkHelper::redactedUrlForLogging(uri);

receiver->setUri(uri);

Expand Down Expand Up @@ -899,13 +901,16 @@ void VideoManager::_initVideoReceiver(VideoReceiver *receiver, QQuickWindow *win
});

(void) connect(receiver, &VideoReceiver::onStopComplete, this, [this, receiver](VideoReceiver::STATUS status) {
qCDebug(VideoManagerLog) << "Stop complete" << receiver->name() << receiver->uri() << ", status:" << status;
qCDebug(VideoManagerLog) << "Stop complete" << receiver->name()
<< QGCNetworkHelper::redactedUrlForLogging(receiver->uri())
<< ", status:" << status;
receiver->setStarted(false);
if (status == VideoReceiver::STATUS_INVALID_URL) {
qCDebug(VideoManagerLog) << "Invalid video URL. Not restarting";
} else {
QTimer::singleShot(1000, receiver, [this, receiver]() {
qCDebug(VideoManagerLog) << "Restarting video receiver" << receiver->name() << receiver->uri();
qCDebug(VideoManagerLog) << "Restarting video receiver" << receiver->name()
<< QGCNetworkHelper::redactedUrlForLogging(receiver->uri());
_startReceiver(receiver);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ QString writePipelineDot(GstElement* pipeline, const char* tag)
QFile::remove(existing.takeFirst().absoluteFilePath());
}

gchar* data = gst_debug_bin_to_dot_data(GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL);
gchar* data = gst_debug_bin_to_dot_data(GST_BIN(pipeline), kDiagnosticDotGraphDetails);
if (!data)
return {};
const QString fileName = QStringLiteral("%1-%2.dot")
Expand Down
4 changes: 4 additions & 0 deletions src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
#include "GStreamer.h" // VideoDecoderOptions

namespace GStreamer {
/// Automatic field-report graphs omit element properties because source properties can contain credentials.
inline constexpr GstDebugGraphDetails kDiagnosticDotGraphDetails = static_cast<GstDebugGraphDetails>(
GST_DEBUG_GRAPH_SHOW_MEDIA_TYPE | GST_DEBUG_GRAPH_SHOW_CAPS_DETAILS | GST_DEBUG_GRAPH_SHOW_STATES);
Comment on lines +12 to +14

@alireza787b alireza787b Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the two dump paths deliberately separate. Automatic CacheLocation snapshots use the restricted detail mask and omit element properties. Native GST_DEBUG_DUMP_DOT_DIR dumps are an explicit developer opt-in and retain SHOW_ALL, matching the earlier maintainer request to preserve full local diagnostic detail. 65c2209 updates the README to state that these opt-in native dumps can contain credentials and must be treated as sensitive, removing the previous blanket guarantee.


bool isValidRtspUri(const gchar* uri_str);

/// Dump @p pipeline's graph as a rotating .dot under CacheLocation/qgc-pipeline-dot/ for field reports.
Expand Down
15 changes: 10 additions & 5 deletions src/VideoManager/VideoReceiver/GStreamer/GstSourceFactory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "GStreamerHelpers.h"
#include "QGCLoggingCategory.h"
#include "QGCNetworkHelper.h"

QGC_LOGGING_CATEGORY(GstSourceFactoryLog, "Video.GStreamer.GstSourceFactory")

Expand Down Expand Up @@ -295,7 +296,7 @@ void linkPad(GstElement* element, GstPad* pad, gpointer data)
GstElement* buildRtspSource(const QString& uri, const QUrl& sourceUrl, const Config& config, guint latencyMs)
{
if (!GStreamer::isValidRtspUri(uri.toUtf8().constData())) {
qCCritical(GstSourceFactoryLog) << "Invalid RTSP URI:" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
qCWarning(GstSourceFactoryLog) << "Invalid RTSP URI:" << QGCNetworkHelper::redactedUrlForLogging(sourceUrl);
return nullptr;
}

Expand Down Expand Up @@ -336,12 +337,14 @@ GstElement* buildTcpSource(const QUrl& sourceUrl)
{
const int port = sourceUrl.port();
if (!validPort(port)) {
qCCritical(GstSourceFactoryLog) << "Invalid TCP port" << port << "in" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
qCWarning(GstSourceFactoryLog) << "Invalid TCP port" << port << "in"
<< QGCNetworkHelper::redactedUrlForLogging(sourceUrl);
return nullptr;
}
const QString host = sourceUrl.host();
if (host.isEmpty()) {
qCCritical(GstSourceFactoryLog) << "Missing host in TCP URI" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
qCWarning(GstSourceFactoryLog) << "Missing host in TCP URI"
<< QGCNetworkHelper::redactedUrlForLogging(sourceUrl);
return nullptr;
}

Expand All @@ -359,7 +362,8 @@ GstElement* buildUdpSource(const QUrl& sourceUrl, bool isUdpH264, bool isUdpH265
{
const int port = sourceUrl.port();
if (!validPort(port)) {
qCCritical(GstSourceFactoryLog) << "Invalid UDP port" << port << "in" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
qCWarning(GstSourceFactoryLog) << "Invalid UDP port" << port << "in"
<< QGCNetworkHelper::redactedUrlForLogging(sourceUrl);
return nullptr;
}

Expand Down Expand Up @@ -528,7 +532,8 @@ GstElement* create(const QString& uri, const Config& config)
const bool isTcpMPEGTS = (scheme == QLatin1String("tcp"));

if (!isRtsp && !isUdpH264 && !isUdpH265 && !isUdpMPEGTS && !isTcpMPEGTS) {
qCWarning(GstSourceFactoryLog) << "Unsupported URI scheme:" << scheme << "in" << sourceUrl.toDisplayString(QUrl::RemoveUserInfo);
qCWarning(GstSourceFactoryLog) << "Unsupported URI scheme:" << scheme << "in"
<< QGCNetworkHelper::redactedUrlForLogging(sourceUrl);
return nullptr;
}

Expand Down
Loading
Loading