mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-30 12:13:56 +08:00
fix: Comprehensive v2.4.0 security, stability, and quality improvements
Security: - Add SSRF protection via HTTPS-only endpoint validation with private IP blocking - Mask API keys in all log output via safe_log_data() - Sanitize error responses to prevent internal detail leakage - Remove API key pre-population in config flow forms - Harden service schemas with input length limits and type coercion Bug fixes: - Fix temperature=0 rejected due to Python falsy value bug (0 or default) - Fix fire-and-forget race condition in coordinator init (5 async_create_task → awaited async_initialize) - Fix rate limit state not resetting after successful API calls - Fix ConnectionError incorrectly setting is_rate_limited=True - Fix _rotate_history calling async method via sync executor - Fix shutil.move blocking event loop in history migration - Fix async_shutdown removing wrong key from hass.data - Replace os.rename with shutil.move for cross-device safety Improvements: - Add asyncio.Lock for request serialization - Replace async_timeout with stdlib asyncio.timeout (Python 3.12+) - Use SupportsResponse.OPTIONAL for ask_question and get_history services - Use Platform.SENSOR enum instead of string literal - Add strings.json for HA translation framework - Update DEFAULT_ANTHROPIC_MODEL to claude-sonnet-4-6 - Retry only transient errors (429, timeout) in API client Cleanup: - Remove dead code: unused schemas, imports, sync_write_history, check_memory, check_connection - Remove icon copying code from async_setup - Remove unused dependencies from manifest (anthropic, openai, certifi, async-timeout) - Remove empty manifest arrays (bluetooth, mqtt, ssdp, usb, zeroconf) - Fix duplicate JSON keys in 5 translation files - Fix Serbian translation: "Прекључено" → "Искључено" for disconnected state Bump version to 2.4.0
This commit is contained in:
@@ -49,7 +49,7 @@ from .const import (
|
||||
DEFAULT_MAX_HISTORY,
|
||||
CONF_MAX_HISTORY_SIZE,
|
||||
)
|
||||
from .utils import normalize_name # noqa: F401 — re-exported for backward compat
|
||||
from .utils import normalize_name, safe_log_data, validate_endpoint # noqa: F401 — re-exported for backward compat
|
||||
from .providers import get_default_endpoint, get_default_model
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -132,16 +132,15 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
})
|
||||
)
|
||||
|
||||
# Debug log to identify what's in the input
|
||||
_LOGGER.debug(f"Provider step input data: {user_input}")
|
||||
_LOGGER.debug("Provider step input data: %s", safe_log_data(user_input))
|
||||
|
||||
input_copy = user_input.copy()
|
||||
|
||||
# Check if CONF_NAME exists in input_copy and ensure it's not empty
|
||||
if CONF_NAME not in input_copy or not input_copy[CONF_NAME]:
|
||||
_LOGGER.warning(f"Missing name in configuration input: {input_copy}")
|
||||
input_copy[CONF_NAME] = f"gemini_assistant_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
_LOGGER.info(f"Auto-generated name: {input_copy[CONF_NAME]}")
|
||||
_LOGGER.warning("Missing name in configuration input: %s", safe_log_data(input_copy))
|
||||
input_copy[CONF_NAME] = f"assistant_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
_LOGGER.info("Auto-generated name: %s", input_copy[CONF_NAME])
|
||||
|
||||
# Ensure API key is present
|
||||
if CONF_API_KEY not in input_copy or not input_copy[CONF_API_KEY]:
|
||||
@@ -197,7 +196,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="provider",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, get_default_model(self._provider))): str,
|
||||
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, get_default_endpoint(self._provider))): str,
|
||||
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||
@@ -259,7 +258,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="provider",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, get_default_model(self._provider))): str,
|
||||
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, get_default_endpoint(self._provider))): str,
|
||||
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||
@@ -302,7 +301,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="provider",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, get_default_model(self._provider))): str,
|
||||
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, get_default_endpoint(self._provider))): str,
|
||||
# Other fields remain the same
|
||||
@@ -353,9 +352,16 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
|
||||
# Validate endpoint URL (HTTPS-only, SSRF protection)
|
||||
try:
|
||||
endpoint = validate_endpoint(user_input[CONF_API_ENDPOINT])
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Endpoint validation failed: %s", err)
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
headers = self._get_api_headers(user_input)
|
||||
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||
|
||||
if self._provider == API_PROVIDER_GEMINI:
|
||||
if not user_input[CONF_API_KEY]:
|
||||
@@ -434,7 +440,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if key not in entry_data:
|
||||
entry_data[key] = value
|
||||
|
||||
_LOGGER.debug(f"Creating config entry with data: {entry_data}")
|
||||
_LOGGER.debug("Creating config entry with data: %s", safe_log_data(entry_data))
|
||||
|
||||
return self.async_create_entry(
|
||||
title=instance_name,
|
||||
@@ -484,13 +490,20 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
|
||||
# Validate endpoint URL (HTTPS-only, SSRF protection)
|
||||
try:
|
||||
endpoint = validate_endpoint(endpoint)
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Endpoint validation failed: %s", err)
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
|
||||
# For Gemini, just check if API key is present
|
||||
if provider == API_PROVIDER_GEMINI:
|
||||
return True
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
headers = self._get_api_headers(api_key, provider)
|
||||
endpoint = endpoint.rstrip('/')
|
||||
|
||||
check_url = (
|
||||
f"{endpoint}/v1/models" if provider == API_PROVIDER_ANTHROPIC
|
||||
@@ -607,10 +620,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
data = user_input or current_data
|
||||
|
||||
return vol.Schema({
|
||||
vol.Required(
|
||||
CONF_API_KEY,
|
||||
default=data.get(CONF_API_KEY, "")
|
||||
): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(
|
||||
CONF_API_ENDPOINT,
|
||||
default=data.get(CONF_API_ENDPOINT, default_endpoint)
|
||||
|
||||
Reference in New Issue
Block a user