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>
110 lines
3.2 KiB
C++
110 lines
3.2 KiB
C++
/**
|
|
* @file WebRadioJson.cpp
|
|
* @brief WebRadioJson implementation.
|
|
*
|
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
|
*
|
|
* Copyright 2026 Michele Bigi
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* @author Michele Bigi
|
|
* @date 2026-08-08
|
|
*/
|
|
|
|
#include "core/WebRadioJson.hpp"
|
|
|
|
namespace core {
|
|
|
|
namespace {
|
|
|
|
constexpr std::size_t kMaxUrlLength = 200U;
|
|
constexpr std::string_view kHttpPrefix = "http://";
|
|
constexpr std::string_view kHttpsPrefix = "https://";
|
|
|
|
[[nodiscard]] std::string_view extractJsonString(std::string_view json,
|
|
std::string_view key)
|
|
{
|
|
const std::string needle =
|
|
std::string("\"") + std::string(key) + "\":";
|
|
const std::size_t needlePos = json.find(needle);
|
|
if (needlePos == std::string_view::npos) {
|
|
return {};
|
|
}
|
|
std::size_t start = needlePos + needle.size();
|
|
while (start < json.size()
|
|
&& (json[start] == ' ' || json[start] == '\t'
|
|
|| json[start] == '\r' || json[start] == '\n')) {
|
|
++start;
|
|
}
|
|
if (start >= json.size() || json[start] != '"') {
|
|
return {};
|
|
}
|
|
const std::size_t valueStart = start + 1U;
|
|
const std::size_t valueEnd = json.find('"', valueStart);
|
|
if (valueEnd == std::string_view::npos) {
|
|
return {};
|
|
}
|
|
return json.substr(valueStart, valueEnd - valueStart);
|
|
}
|
|
|
|
[[nodiscard]] bool extractJsonBool(std::string_view json,
|
|
std::string_view key, bool& out)
|
|
{
|
|
const std::string needle =
|
|
std::string("\"") + std::string(key) + "\":";
|
|
const std::size_t needlePos = json.find(needle);
|
|
if (needlePos == std::string_view::npos) {
|
|
return false;
|
|
}
|
|
std::size_t start = needlePos + needle.size();
|
|
while (start < json.size()
|
|
&& (json[start] == ' ' || json[start] == '\t'
|
|
|| json[start] == '\r' || json[start] == '\n')) {
|
|
++start;
|
|
}
|
|
const std::string_view rest = json.substr(start);
|
|
if (rest.starts_with("true")) {
|
|
out = true;
|
|
return true;
|
|
}
|
|
if (rest.starts_with("false")) {
|
|
out = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::expected<WebRadioConfig, ParseError> parseWebRadioConfigJson(
|
|
std::string_view json)
|
|
{
|
|
bool enabled = false;
|
|
if (!extractJsonBool(json, "enabled", enabled)) {
|
|
return std::unexpected(ParseError::MissingField);
|
|
}
|
|
|
|
const std::string_view url = extractJsonString(json, "url");
|
|
const bool isHttp = 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 WebRadioConfig{.enabled = enabled, .url = std::string(url)};
|
|
}
|
|
|
|
std::string serializeWebRadioConfigJson(const WebRadioConfig& config)
|
|
{
|
|
return std::string(R"({"enabled":)") + (config.enabled ? "true" : "false")
|
|
+ R"(,"url":")" + config.url + "\"}";
|
|
}
|
|
|
|
std::string serializeWebRadioErrorJson(std::string_view reason)
|
|
{
|
|
return std::string(R"({"status":"error","reason":")")
|
|
+ std::string(reason) + "\"}";
|
|
}
|
|
|
|
} // namespace core
|