mirror of
https://github.com/esphome/esphome.git
synced 2025-07-28 14:16:40 +00:00
Make API ConnectRequest optional for passwordless connections (#9445)
This commit is contained in:
parent
e012fd5b32
commit
5c2dea79ef
@ -1435,6 +1435,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
|
||||
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void APIConnection::complete_authentication_() {
|
||||
// Early return if already authenticated
|
||||
if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
this->send_time_request();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
||||
this->client_info_ = msg.client_info;
|
||||
this->client_peername_ = this->helper_->getpeername();
|
||||
@ -1450,7 +1468,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
||||
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
||||
resp.name = App.get_name();
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
// Password required - wait for authentication
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED);
|
||||
#else
|
||||
// No password configured - auto-authenticate
|
||||
this->complete_authentication_();
|
||||
#endif
|
||||
|
||||
return resp;
|
||||
}
|
||||
ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
||||
@ -1463,23 +1488,14 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
||||
// bool invalid_password = 1;
|
||||
resp.invalid_password = !correct;
|
||||
if (correct) {
|
||||
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
this->send_time_request();
|
||||
}
|
||||
#endif
|
||||
this->complete_authentication_();
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
||||
DeviceInfoResponse resp{};
|
||||
#ifdef USE_API_PASSWORD
|
||||
resp.uses_password = this->parent_->uses_password();
|
||||
resp.uses_password = true;
|
||||
#else
|
||||
resp.uses_password = false;
|
||||
#endif
|
||||
|
@ -273,6 +273,9 @@ class APIConnection : public APIServerConnection {
|
||||
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
// Helper function to fill common entity info fields
|
||||
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
|
||||
// Set common fields that are shared by all entity types
|
||||
|
@ -219,8 +219,6 @@ void APIServer::dump_config() {
|
||||
}
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool APIServer::uses_password() const { return !this->password_.empty(); }
|
||||
|
||||
bool APIServer::check_password(const std::string &password) const {
|
||||
// depend only on input password length
|
||||
const char *a = this->password_.c_str();
|
||||
|
@ -39,7 +39,6 @@ class APIServer : public Component, public Controller {
|
||||
bool teardown() override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool check_password(const std::string &password) const;
|
||||
bool uses_password() const;
|
||||
void set_password(const std::string &password);
|
||||
#endif
|
||||
void set_port(uint16_t port);
|
||||
|
14
tests/integration/fixtures/host_mode_api_password.yaml
Normal file
14
tests/integration/fixtures/host_mode_api_password.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: host-mode-api-password
|
||||
host:
|
||||
api:
|
||||
password: "test_password_123"
|
||||
logger:
|
||||
level: DEBUG
|
||||
# Test sensor to verify connection works
|
||||
sensor:
|
||||
- platform: template
|
||||
name: Test Sensor
|
||||
id: test_sensor
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s
|
53
tests/integration/test_host_mode_api_password.py
Normal file
53
tests/integration/test_host_mode_api_password.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Integration test for API password authentication."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import APIConnectionError
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_mode_api_password(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test API authentication with password."""
|
||||
async with run_compiled(yaml_config):
|
||||
# Connect with correct password
|
||||
async with api_client_connected(password="test_password_123") as client:
|
||||
# Verify we can get device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.uses_password is True
|
||||
assert device_info.name == "host-mode-api-password"
|
||||
|
||||
# Subscribe to states to ensure authenticated connection works
|
||||
loop = asyncio.get_running_loop()
|
||||
state_future: asyncio.Future[bool] = loop.create_future()
|
||||
states = {}
|
||||
|
||||
def on_state(state):
|
||||
states[state.key] = state
|
||||
if not state_future.done():
|
||||
state_future.set_result(True)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Wait for at least one state with timeout
|
||||
try:
|
||||
await asyncio.wait_for(state_future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail("No states received within timeout")
|
||||
|
||||
# Should have received at least one state (the test sensor)
|
||||
assert len(states) > 0
|
||||
|
||||
# Test with wrong password - should fail
|
||||
with pytest.raises(APIConnectionError, match="Invalid password"):
|
||||
async with api_client_connected(password="wrong_password"):
|
||||
pass # Should not reach here
|
Loading…
x
Reference in New Issue
Block a user