mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 19:44:08 +00:00
[audio_http][core] Add ca_certificate_path to audio_http media source (#19392)
This commit is contained in:
@@ -33,6 +33,9 @@ void AudioHTTPMediaSource::dump_config() {
|
||||
" Persistent Ring Buffer: %s\n"
|
||||
" Decoder Task Stack in PSRAM: %s",
|
||||
this->buffer_size_, YESNO(this->persistent_ring_buffer_), YESNO(this->decoder_task_stack_in_psram_));
|
||||
#ifdef USE_AUDIO_HTTP_CA_CERTIFICATE
|
||||
ESP_LOGCONFIG(TAG, " Custom CA Certificate: %s", YESNO(this->http_ca_certificate_ != nullptr));
|
||||
#endif
|
||||
}
|
||||
|
||||
void AudioHTTPMediaSource::setup() {
|
||||
@@ -52,6 +55,12 @@ void AudioHTTPMediaSource::setup() {
|
||||
config.reader_stack_size = READER_TASK_STACK_SIZE;
|
||||
config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
|
||||
config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
|
||||
#ifdef USE_AUDIO_HTTP_CA_CERTIFICATE
|
||||
// micro-decoder verifies HTTPS against this PEM only, skipping the built-in certificate bundle.
|
||||
if (this->http_ca_certificate_ != nullptr) {
|
||||
config.http_ca_certificate = this->http_ca_certificate_;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
|
||||
if (this->decoder_ == nullptr) {
|
||||
|
||||
@@ -34,6 +34,11 @@ class AudioHTTPMediaSource final : public Component,
|
||||
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
|
||||
void set_persistent_ring_buffer(bool persistent) { this->persistent_ring_buffer_ = persistent; }
|
||||
#ifdef USE_AUDIO_HTTP_CA_CERTIFICATE
|
||||
/// PEM-encoded CA certificate(s) used as the sole trust anchor for HTTPS playback URLs,
|
||||
/// replacing the built-in certificate bundle.
|
||||
void set_http_ca_certificate(const char *ca_certificate) { this->http_ca_certificate_ = ca_certificate; }
|
||||
#endif
|
||||
|
||||
// MediaSource interface implementation
|
||||
bool play_uri(const std::string &uri) override;
|
||||
@@ -56,6 +61,9 @@ class AudioHTTPMediaSource final : public Component,
|
||||
std::atomic<bool> pause_{false};
|
||||
bool decoder_task_stack_in_psram_{false};
|
||||
bool persistent_ring_buffer_{false};
|
||||
#ifdef USE_AUDIO_HTTP_CA_CERTIFICATE
|
||||
const char *http_ca_certificate_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_http
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, media_source, psram
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
AUTO_LOAD = ["audio"]
|
||||
|
||||
CONF_PERSISTENT_RING_BUFFER = "persistent_ring_buffer"
|
||||
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
|
||||
CONF_HTTP_REQUEST = "http_request"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
|
||||
audio_http_ns = cg.esphome_ns.namespace("audio_http")
|
||||
AudioHTTPMediaSource = audio_http_ns.class_(
|
||||
@@ -20,6 +29,38 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _inherit_ca_certificate_path(config: ConfigType) -> ConfigType:
|
||||
# Default to the CA certificate configured on the http_request component so
|
||||
# HTTPS playback verifies against the same trust anchor without repeating
|
||||
# the option on every source. Needed because audio_http sources are often
|
||||
# declared by device packages and cannot be extended from the device config.
|
||||
# The PEM replaces the built-in certificate bundle as the sole trust anchor,
|
||||
# so make the inheritance visible in the log.
|
||||
if CONF_CA_CERTIFICATE_PATH in config:
|
||||
return config
|
||||
fconf = fv.full_config.get()
|
||||
if CONF_HTTP_REQUEST not in fconf:
|
||||
return config
|
||||
http_request_config = fconf[CONF_HTTP_REQUEST]
|
||||
# Mirror http_request's own semantics: it only applies its CA when SSL
|
||||
# verification is enabled, so neither does the inheritance.
|
||||
if not http_request_config.get(CONF_VERIFY_SSL, True):
|
||||
return config
|
||||
if ca_cert_path := http_request_config.get(CONF_CA_CERTIFICATE_PATH):
|
||||
_LOGGER.info(
|
||||
"audio_http source '%s' is inheriting ca_certificate_path from the "
|
||||
"http_request component; HTTPS playback will verify against that PEM "
|
||||
"instead of the built-in certificate bundle",
|
||||
config[CONF_ID].id,
|
||||
)
|
||||
# Already validated by http_request's schema; the value is a resolved Path.
|
||||
config[CONF_CA_CERTIFICATE_PATH] = ca_cert_path
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _inherit_ca_certificate_path
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_source.media_source_schema(
|
||||
AudioHTTPMediaSource,
|
||||
@@ -31,6 +72,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_PERSISTENT_RING_BUFFER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.file_,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -47,5 +89,16 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
psram.request_external_task_stack()
|
||||
|
||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
cg.add(var.set_persistent_ring_buffer(config[CONF_PERSISTENT_RING_BUFFER]))
|
||||
|
||||
# Embed the certificate content, like http_request does. Passed to
|
||||
# micro_decoder's DecoderConfig::http_ca_certificate, which then uses it as
|
||||
# the sole trust anchor for HTTPS playback URLs instead of the certificate
|
||||
# bundle.
|
||||
if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH):
|
||||
cg.add_define("USE_AUDIO_HTTP_CA_CERTIFICATE")
|
||||
with Path(ca_cert_path).open(encoding="utf-8") as f:
|
||||
ca_cert_content = f.read()
|
||||
cg.add(var.set_http_ca_certificate(ca_cert_content))
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
#define AUDIO_FILE_MAX_FILES 4
|
||||
#define USE_AUDIO_DAC
|
||||
#define USE_AUDIO_FLAC_SUPPORT
|
||||
#define USE_AUDIO_HTTP_CA_CERTIFICATE
|
||||
#define USE_AUDIO_MP3_SUPPORT
|
||||
#define USE_AUDIO_OPUS_SUPPORT
|
||||
#define USE_AUDIO_WAV_SUPPORT
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
media_source:
|
||||
- platform: audio_http
|
||||
id: audio_http_ca_source
|
||||
ca_certificate_path: $component_dir/test_ca.pem
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,13 @@
|
||||
# http_request requires a network component; audio_http's common.yaml declares none.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
http_request:
|
||||
ca_certificate_path: $component_dir/test_ca.pem
|
||||
|
||||
media_source:
|
||||
- platform: audio_http
|
||||
id: audio_http_inherit_ca_source
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBkTCB+wIJAKHBfpegPjMCMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnVu
|
||||
dXNlZDAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM
|
||||
BnVudXNlZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5mMUB1hOgLmlnXtsvcGMP
|
||||
XkhAqZaR0dDPW5OS8VEopWLJCX9Y0cvNCqiDI8cnP8pP8XJGU1hGLvA5PJzWnWZz
|
||||
AgMBAAGjUzBRMB0GA1UdDgQWBBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAfBgNVHSME
|
||||
GDAWgBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
|
||||
SIb3DQEBCwUAA0EAKqZFf6+f8FPDbKyPCpssquojgn7fEXqr/I/yz0R5CowGdMms
|
||||
H3WH3aKP4lLSHdPTBtfIoJi3gEIZjFxp3S1TWw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Tests for the audio_http media source's ca_certificate_path inheritance."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.components.audio_http.media_source import _inherit_ca_certificate_path
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def _source_config() -> ConfigType:
|
||||
return {"id": ID("audio_http_source")}
|
||||
|
||||
|
||||
def _run(full_config: dict, config: ConfigType) -> ConfigType:
|
||||
token = fv.full_config.set(full_config)
|
||||
try:
|
||||
return _inherit_ca_certificate_path(config)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_explicit_option_wins() -> None:
|
||||
"""A source with its own ca_certificate_path is left untouched."""
|
||||
config = {"id": ID("s"), "ca_certificate_path": Path("own.pem")}
|
||||
result = _run(
|
||||
{"http_request": {"verify_ssl": True, "ca_certificate_path": Path("hr.pem")}},
|
||||
config,
|
||||
)
|
||||
assert result["ca_certificate_path"] == Path("own.pem")
|
||||
|
||||
|
||||
def test_inherited_from_http_request() -> None:
|
||||
"""Without its own option, the source picks up http_request's CA path."""
|
||||
result = _run(
|
||||
{
|
||||
"http_request": {
|
||||
"verify_ssl": True,
|
||||
"ca_certificate_path": Path("hr.pem"),
|
||||
}
|
||||
},
|
||||
_source_config(),
|
||||
)
|
||||
assert result["ca_certificate_path"] == Path("hr.pem")
|
||||
|
||||
|
||||
def test_no_http_request_component() -> None:
|
||||
"""No http_request block means no inheritance."""
|
||||
result = _run({}, _source_config())
|
||||
assert "ca_certificate_path" not in result
|
||||
|
||||
|
||||
def test_http_request_without_ca() -> None:
|
||||
"""An http_request block without ca_certificate_path means no inheritance."""
|
||||
result = _run({"http_request": {"verify_ssl": True}}, _source_config())
|
||||
assert "ca_certificate_path" not in result
|
||||
|
||||
|
||||
def test_not_inherited_when_verify_ssl_disabled() -> None:
|
||||
"""verify_ssl: false disables the CA for http_request itself, so the
|
||||
inheritance must skip it too instead of silently pinning playback."""
|
||||
result = _run(
|
||||
{
|
||||
"http_request": {
|
||||
"verify_ssl": False,
|
||||
"ca_certificate_path": Path("hr.pem"),
|
||||
}
|
||||
},
|
||||
_source_config(),
|
||||
)
|
||||
assert "ca_certificate_path" not in result
|
||||
Reference in New Issue
Block a user