Files
ha-text-ai/custom_components/ha_text_ai/utils.py
T
SMKRV 998103ce73 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
2026-03-12 01:24:01 +03:00

61 lines
1.8 KiB
Python

"""
Utility functions for HA Text AI integration.
@license: CC BY-NC-SA 4.0 International
@author: SMKRV
@github: https://github.com/smkrv/ha-text-ai
@source: https://github.com/smkrv/ha-text-ai
"""
import ipaddress
from typing import Any
from urllib.parse import urlparse
from homeassistant.const import CONF_API_KEY
def normalize_name(name: str) -> str:
"""Normalize name to conform to HA naming convention using underscores."""
normalized = ''.join(c if c.isalnum() or c == '_' else '_' for c in name)
normalized = '_'.join(filter(None, normalized.split('_')))
return normalized.lower()
def safe_log_data(
data: dict[str, Any],
sensitive_keys: tuple[str, ...] = (CONF_API_KEY,),
) -> dict[str, Any]:
"""Filter sensitive keys from data for safe logging."""
return {k: "***" if k in sensitive_keys else v for k, v in data.items()}
def validate_endpoint(endpoint: str) -> str:
"""Validate API endpoint URL for security.
Ensures HTTPS-only and blocks private/reserved IP ranges (SSRF protection).
Returns the validated endpoint stripped of trailing slashes.
Raises:
ValueError: If the endpoint fails validation.
"""
parsed = urlparse(endpoint)
if parsed.scheme not in ("https",):
raise ValueError("Only HTTPS endpoints are allowed")
hostname = parsed.hostname
if not hostname:
raise ValueError("Invalid endpoint URL: no hostname")
# Block private/reserved IPs
try:
addr = ipaddress.ip_address(hostname)
if addr.is_private or addr.is_reserved or addr.is_loopback or addr.is_link_local:
raise ValueError("Private/reserved IP addresses are not allowed")
except ValueError as e:
if "not allowed" in str(e):
raise
# Not an IP address — it's a hostname, which is fine
return endpoint.rstrip("/")