Fix web radio streaming: HTTPS URLs were rejected outright

POST /api/streaming's JSON parser only accepted "http://" URLs,
rejecting any "https://" URL with invalid_json before ever attempting
a connection. Nearly every real internet radio stream today is HTTPS-
only, so this made the feature fail for essentially any station a
user would actually try.

Two changes were needed together: the parser now accepts both http://
and https://, and web_radio_stream.cpp's esp_http_client now attaches
ESP-IDF's built-in CA certificate bundle (crt_bundle_attach) so the
TLS handshake actually verifies -- CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
was already enabled in sdkconfig but never wired up here. Requires
adding mbedtls to main's PRIV_REQUIRES (esp_crt_bundle.h lives there).

Confirmed live: an https:// stream URL now gets accepted by the API
and produces a real HTTP response (404, from a guessed-wrong path) --
before this fix it never reached the network at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 00:27:02 +02:00
co-authored by Claude Sonnet 5
parent 012d2a6267
commit b46dcea698
5 changed files with 61 additions and 26 deletions
@@ -19,6 +19,7 @@ namespace {
constexpr std::size_t kMaxUrlLength = 200U; constexpr std::size_t kMaxUrlLength = 200U;
constexpr std::string_view kHttpPrefix = "http://"; constexpr std::string_view kHttpPrefix = "http://";
constexpr std::string_view kHttpsPrefix = "https://";
[[nodiscard]] std::string_view extractJsonString(std::string_view json, [[nodiscard]] std::string_view extractJsonString(std::string_view json,
std::string_view key) std::string_view key)
@@ -84,8 +85,9 @@ std::expected<WebRadioConfig, ParseError> parseWebRadioConfigJson(
} }
const std::string_view url = extractJsonString(json, "url"); const std::string_view url = extractJsonString(json, "url");
if (url.empty() || url.size() > kMaxUrlLength const bool isHttp = url.substr(0, kHttpPrefix.size()) == kHttpPrefix;
|| url.substr(0, kHttpPrefix.size()) != kHttpPrefix) { const bool isHttps = url.substr(0, kHttpsPrefix.size()) == kHttpsPrefix;
if (url.empty() || url.size() > kMaxUrlLength || !(isHttp || isHttps)) {
return std::unexpected(ParseError::InvalidJson); return std::unexpected(ParseError::InvalidJson);
} }
@@ -40,9 +40,18 @@ namespace {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
const auto badScheme = core::parseWebRadioConfigJson( const auto badScheme = core::parseWebRadioConfigJson(
R"({"enabled":true,"url":"https://example.com/x.mp3"})"); R"({"enabled":true,"url":"ftp://example.com/x.mp3"})");
if (badScheme) { if (badScheme) {
std::cerr << "non-http scheme should fail\n"; std::cerr << "non-http(s) scheme should fail\n";
return EXIT_FAILURE;
}
// Most public internet radio streams are HTTPS-only; rejecting the
// scheme outright (as this parser used to) made streaming unusable for
// essentially any real station.
const auto httpsOk = core::parseWebRadioConfigJson(
R"({"enabled":true,"url":"https://example.com/x.mp3"})");
if (!httpsOk || httpsOk->url != "https://example.com/x.mp3") {
std::cerr << "https url should parse\n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }
const auto malformed = core::parseWebRadioConfigJson("not json"); const auto malformed = core::parseWebRadioConfigJson("not json");
+38 -21
View File
@@ -176,26 +176,6 @@ quando arriva un nuovo nome/messaggio RDS durante la riproduzione.
--- ---
## 5. Stile UI — Apple, minimalista ma con una sezione grafica curata
- Componenti nativi SwiftUI: `Picker` segmented per la sorgente, `Slider` con
`.tint()` per volume/bass/stereo, `List`/`Form` in stile Impostazioni per le
stazioni e le opzioni tecniche. Niente controlli custom pesanti o griglie di
bottoni non standard.
- Organizza per tab/sezione logica, non tutto in una schermata:
- **Ascolto**: sorgente attiva, volume, stazione corrente.
- **Suono**: EQ 6 bande, Bass Boost, Stereo Spread.
- **Stazioni**: lista unificata FM+DAB (vedi §4), con scan.
- **Bluetooth**: pairing, dispositivo connesso.
- **Diagnostica**: tono di test, dettagli tecnici/versione firmware — non
mescolare con i controlli quotidiani.
- Una sezione "grafica" curata è benvenuta (es. una card "Now Playing" con
sfondo sfumato/blur, animazione leggera sul cambio sorgente). Ora **puoi**
agganciarla a dati reali di livello audio — vedi §7, l'endpoint VU-meter è
disponibile da oggi.
---
## 7. VU-meter — nuovo, disponibile da oggi ## 7. VU-meter — nuovo, disponibile da oggi
``` ```
@@ -225,7 +205,44 @@ uscita), non un'animazione finta.
--- ---
## 8. Checklist di autoverifica prima di considerare il lavoro finito ## 8. Scan FM completo — NON è bloccato, è solo lento (86s misurati)
```
POST /api/tuner/scan/full
```
Fa una scansione dell'intera banda FM (fino a 60 canali candidati, con
pausa+lettura RDS per ciascuno) e risponde **una sola volta alla fine**,
misurato: **~86 secondi** per uno scan completo. Non è un bug, è il tempo
reale che serve per farlo bene (RDS incluso).
**Se l'app usa un timeout HTTP standard (30-60s), questa richiesta scade
prima che il firmware finisca** — la request fallisce lato client, ma il
firmware nel frattempo continua e completa comunque (il risultato però va
perso perché il client non lo aspetta più). Sembra "bloccato", ma non lo è.
**Azione richiesta**: per questa chiamata specifica, imposta un timeout di
almeno **120 secondi** sulla request HTTP, e mostra un indicatore "scansione
in corso..." per tutta la durata (non un caricamento breve). `POST
/api/tuner/scan` (senza `/full`, per una singola stazione con filtro nome)
è invece rapido, timeout normale va bene.
---
## 8bis. Streaming web radio — bug corretto, ora accetta HTTPS
`POST /api/streaming {"enabled":true,"url":"..."}` **prima rifiutava
categoricamente qualsiasi URL `https://`** (errore `invalid_json`), accettando
solo `http://` — dato che quasi tutte le radio via internet reali sono
HTTPS-only, questo probabilmente era il motivo per cui "qualsiasi cosa si
faccia" dava errore. Corretto oggi: ora accetta sia `http://` che `https://`,
e il firmware verifica il certificato TLS con la CA bundle integrata di
ESP-IDF. Nessun cambio di contratto per l'app — stessa forma JSON di prima,
semplicemente ora funziona anche con URL HTTPS.
---
## 9. Checklist di autoverifica prima di considerare il lavoro finito
- [ ] Cambiare sorgente da Radio a Bluetooth nell'app cambia davvero l'audio - [ ] Cambiare sorgente da Radio a Bluetooth nell'app cambia davvero l'audio
sul dispositivo reale (non solo lo stato locale dell'app). sul dispositivo reale (non solo lo stato locale dell'app).
+1 -1
View File
@@ -11,7 +11,7 @@ idf_component_register(
"$<$<BOOL:${CONFIG_ESP32_I2S_TEST_TONE}>:esp32_i2s_test_tone.cpp>" "$<$<BOOL:${CONFIG_ESP32_I2S_TEST_TONE}>:esp32_i2s_test_tone.cpp>"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035 bluetooth station integration ota eeprom24aa webradio driver REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035 bluetooth station integration ota eeprom24aa webradio driver
PRIV_REQUIRES esp_timer esp_http_client PRIV_REQUIRES esp_timer esp_http_client mbedtls
) )
if(CONFIG_TEST_FIRMWARE) if(CONFIG_TEST_FIRMWARE)
+7
View File
@@ -13,6 +13,7 @@
#include "esp32_i2s_sink.hpp" #include "esp32_i2s_sink.hpp"
#include "webradio/WebRadioService.hpp" #include "webradio/WebRadioService.hpp"
#include "esp_crt_bundle.h"
#include "esp_http_client.h" #include "esp_http_client.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@@ -48,6 +49,12 @@ struct InputBuffer {
esp_http_client_config_t cfg{}; esp_http_client_config_t cfg{};
cfg.url = url.c_str(); cfg.url = url.c_str();
cfg.timeout_ms = kHttpTimeoutMs; cfg.timeout_ms = kHttpTimeoutMs;
// Most public internet radio streams are HTTPS-only today; esp_http_client
// needs an explicit trust anchor for TLS verification or the handshake
// fails outright. CONFIG_MBEDTLS_CERTIFICATE_BUNDLE is already enabled
// (sdkconfig), so attach ESP-IDF's built-in CA bundle -- this is a no-op
// for plain http:// URLs.
cfg.crt_bundle_attach = esp_crt_bundle_attach;
esp_http_client_handle_t client = esp_http_client_init(&cfg); esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (client == nullptr) { if (client == nullptr) {
ESP_LOGE(kTag, "esp_http_client_init failed"); ESP_LOGE(kTag, "esp_http_client_init failed");