mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 14:24:14 +02:00
release: 1.0.4 security hardening and stability fixes
Validate manual stream URLs (http/https only), restrict notification cover downloads to HTTPS, and add transfer timeouts for metadata and cover fetches. Document playback, tray, MPRIS, and QML refactor changes in CHANGELOG and AppStream metadata.
This commit is contained in:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.0.4] - 2026-06-20
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Validate manual station URLs: only `http://` and `https://` schemes are accepted.
|
||||||
|
- Restrict notification cover art downloads to HTTPS URLs.
|
||||||
|
- Add network transfer timeouts to ICY metadata, cover art, and notification fetches.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix playback index for history, resume, and next/previous navigation.
|
||||||
|
- Fix Radio Browser API race where stale responses could overwrite newer results.
|
||||||
|
- Suppress error banner when cached station data is still available.
|
||||||
|
- Fix search filter persisting across page changes and compact-mode debounce.
|
||||||
|
- Fix stale list index after station list reload.
|
||||||
|
- Fix Wayland system tray menu using native `QSystemTrayIcon`.
|
||||||
|
- Fix MPRIS metadata publishing for desktop media widgets (e.g. PlasMusic).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactor monolithic `Main.qml` into reusable QML components and `BearTheme`.
|
||||||
|
- Remove unused Kirigami, I18n, and CoreAddons dependencies.
|
||||||
|
- Add backend unit tests for playback navigation and API race handling.
|
||||||
|
|
||||||
## [1.0.3] - 2026-05-30
|
## [1.0.3] - 2026-05-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
project(bearwave VERSION 1.0.3 LANGUAGES CXX)
|
project(bearwave VERSION 1.0.4 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -59,6 +59,16 @@
|
|||||||
</provides>
|
</provides>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.0.4" date="2026-06-20">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Security: validate manual stream URLs (http/https only) and HTTPS-only cover downloads</li>
|
||||||
|
<li>Security: add network transfer timeouts for metadata and cover fetches</li>
|
||||||
|
<li>Fixed playback navigation, API race conditions, Wayland tray, and MPRIS metadata</li>
|
||||||
|
<li>Refactored QML UI into modular components</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="1.0.3" date="2026-05-30">
|
<release version="1.0.3" date="2026-05-30">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ void CoverArtFetcher::fetch(const QString &artist, const QString &title)
|
|||||||
url.setQuery(query);
|
url.setQuery(query);
|
||||||
|
|
||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
|
request.setTransferTimeout(10000);
|
||||||
m_currentReply = m_networkManager->get(request);
|
m_currentReply = m_networkManager->get(request);
|
||||||
connect(m_currentReply, &QNetworkReply::finished, this, &CoverArtFetcher::onReplyFinished);
|
connect(m_currentReply, &QNetworkReply::finished, this, &CoverArtFetcher::onReplyFinished);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ void IcyReader::start(const QString &url)
|
|||||||
request.setRawHeader("User-Agent", "VLC/3.0.16 LibVLC/3.0.16");
|
request.setRawHeader("User-Agent", "VLC/3.0.16 LibVLC/3.0.16");
|
||||||
// Streams often use 302 redirects to load balancers/relays
|
// Streams often use 302 redirects to load balancers/relays
|
||||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
request.setTransferTimeout(15000);
|
||||||
|
|
||||||
m_reply = m_nam->get(request);
|
m_reply = m_nam->get(request);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ int main(int argc, char *argv[])
|
|||||||
QApplication::setApplicationName(QStringLiteral("BearWave"));
|
QApplication::setApplicationName(QStringLiteral("BearWave"));
|
||||||
QApplication::setDesktopFileName(QStringLiteral("de.nerdbear.bearwave"));
|
QApplication::setDesktopFileName(QStringLiteral("de.nerdbear.bearwave"));
|
||||||
QApplication::setOrganizationName(QStringLiteral("BearWave"));
|
QApplication::setOrganizationName(QStringLiteral("BearWave"));
|
||||||
QApplication::setApplicationVersion(QStringLiteral("1.0.3"));
|
QApplication::setApplicationVersion(QStringLiteral("1.0.4"));
|
||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ void NotificationManager::onTrackInfoChanged()
|
|||||||
|
|
||||||
void NotificationManager::downloadCover(const QString &url)
|
void NotificationManager::downloadCover(const QString &url)
|
||||||
{
|
{
|
||||||
|
if (!url.startsWith(QLatin1String("https://"))) {
|
||||||
|
qDebug() << "Skipping cover download for non-HTTPS URL";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_currentReply) {
|
if (m_currentReply) {
|
||||||
QNetworkReply *reply = m_currentReply;
|
QNetworkReply *reply = m_currentReply;
|
||||||
m_currentReply = nullptr;
|
m_currentReply = nullptr;
|
||||||
@@ -133,6 +138,7 @@ void NotificationManager::downloadCover(const QString &url)
|
|||||||
|
|
||||||
m_pendingCoverUrl = url;
|
m_pendingCoverUrl = url;
|
||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
|
request.setTransferTimeout(10000);
|
||||||
m_currentReply = m_networkManager->get(request);
|
m_currentReply = m_networkManager->get(request);
|
||||||
connect(m_currentReply, &QNetworkReply::finished, this, &NotificationManager::onDownloadFinished);
|
connect(m_currentReply, &QNetworkReply::finished, this, &NotificationManager::onDownloadFinished);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kRecentLimit = 20;
|
constexpr int kRecentLimit = 20;
|
||||||
@@ -20,6 +21,16 @@ QString appConfigDir()
|
|||||||
{
|
{
|
||||||
return QDir::homePath() + QStringLiteral("/.config/bearwave");
|
return QDir::homePath() + QStringLiteral("/.config/bearwave");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isAllowedStreamUrl(const QString &urlString)
|
||||||
|
{
|
||||||
|
const QUrl url(urlString.trimmed());
|
||||||
|
if (!url.isValid() || url.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QString scheme = url.scheme().toLower();
|
||||||
|
return scheme == QStringLiteral("http") || scheme == QStringLiteral("https");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioBackend::RadioBackend(QObject *parent)
|
RadioBackend::RadioBackend(QObject *parent)
|
||||||
@@ -409,7 +420,12 @@ void RadioBackend::addManualStation(const QString &name, const QString &url, con
|
|||||||
if (name.trimmed().isEmpty() || url.trimmed().isEmpty()) {
|
if (name.trimmed().isEmpty() || url.trimmed().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isAllowedStreamUrl(url)) {
|
||||||
|
setLastError(tr("Stream URL must use http:// or https://"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastError(QString());
|
||||||
RadioStation *station = new RadioStation(this);
|
RadioStation *station = new RadioStation(this);
|
||||||
station->setName(name.trimmed());
|
station->setName(name.trimmed());
|
||||||
station->setUrl(url.trimmed());
|
station->setUrl(url.trimmed());
|
||||||
@@ -848,7 +864,12 @@ void RadioBackend::editManualStation(QObject *stationObj, const QString &name, c
|
|||||||
if (!station || name.trimmed().isEmpty() || url.trimmed().isEmpty()) {
|
if (!station || name.trimmed().isEmpty() || url.trimmed().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isAllowedStreamUrl(url)) {
|
||||||
|
setLastError(tr("Stream URL must use http:// or https://"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastError(QString());
|
||||||
QString oldUrl = station->url();
|
QString oldUrl = station->url();
|
||||||
QString oldUrlResolved = station->urlResolved();
|
QString oldUrlResolved = station->urlResolved();
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ private slots:
|
|||||||
void cached_load_keeps_error_clear_after_network_failure();
|
void cached_load_keeps_error_clear_after_network_failure();
|
||||||
void list_reload_resyncs_index_for_playing_station();
|
void list_reload_resyncs_index_for_playing_station();
|
||||||
void list_next_disabled_when_playing_station_not_in_new_list();
|
void list_next_disabled_when_playing_station_not_in_new_list();
|
||||||
|
void manual_station_rejects_unsafe_url_scheme();
|
||||||
|
void manual_station_accepts_http_and_https();
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -302,5 +304,29 @@ void RadioBackendPlaybackTest::list_next_disabled_when_playing_station_not_in_ne
|
|||||||
QCOMPARE(backend.currentStationUrl(), topUrl);
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RadioBackendPlaybackTest::manual_station_rejects_unsafe_url_scheme()
|
||||||
|
{
|
||||||
|
RadioBackend backend;
|
||||||
|
|
||||||
|
backend.addManualStation(QStringLiteral("Unsafe"), QStringLiteral("file:///etc/passwd"), QStringLiteral("DE"));
|
||||||
|
QCOMPARE(backend.stations().size(), 0);
|
||||||
|
QVERIFY(!backend.lastError().isEmpty());
|
||||||
|
|
||||||
|
backend.addManualStation(QStringLiteral("FTP"), QStringLiteral("ftp://example.com/stream"), QStringLiteral("DE"));
|
||||||
|
QCOMPARE(backend.stations().size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadioBackendPlaybackTest::manual_station_accepts_http_and_https()
|
||||||
|
{
|
||||||
|
RadioBackend backend;
|
||||||
|
|
||||||
|
backend.addManualStation(QStringLiteral("HTTP"), QStringLiteral("http://example.com/stream"), QStringLiteral("DE"));
|
||||||
|
QCOMPARE(backend.stations().size(), 1);
|
||||||
|
QCOMPARE(backend.lastError(), QString());
|
||||||
|
|
||||||
|
backend.addManualStation(QStringLiteral("HTTPS"), QStringLiteral("https://example.com/stream"), QStringLiteral("DE"));
|
||||||
|
QCOMPARE(backend.stations().size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_MAIN(RadioBackendPlaybackTest)
|
QTEST_MAIN(RadioBackendPlaybackTest)
|
||||||
#include "radiobackend_playback_test.moc"
|
#include "radiobackend_playback_test.moc"
|
||||||
Reference in New Issue
Block a user