fix: Round 2 review findings — async SSRF, error sanitization, constants

Security:
- Make validate_endpoint async with hass.async_add_executor_job for DNS resolution
- Use _RestrictedIPError instead of fragile string matching for IP check flow
- Add is_multicast/is_unspecified to SSRF IP restriction checks
- Remove resolved private IP from error messages (generic message)
- Remove raw endpoint from __init__.py error log
- Sanitize error messages in metrics: strip URLs, API keys, Gemini key patterns
- Truncate API error response bodies before logging (512 chars)
- Use generic error messages in Gemini exception handlers (no str(e) interpolation)
- Pass api_key as explicit APIClient constructor parameter (not from header)
- Add defensive validation: Gemini provider requires api_key at construction

Code quality:
- Add constants: DEFAULT_INSTANCE_NAME, MIN/MAX_CONTEXT_MESSAGES, MIN/MAX_HISTORY_SIZE
- Replace all hardcoded schema ranges with named constants
- Move datetime import to module level in history.py
- Remove unused full_history/full_history_available keys
- Chain socket.gaierror properly with raise...from
This commit is contained in:
SMKRV
2026-03-12 01:57:49 +03:00
parent c54bfcff3b
commit 31560a8835
8 changed files with 72 additions and 38 deletions
+11 -7
View File
@@ -39,6 +39,7 @@ class APIClient:
api_provider: str,
model: str,
api_timeout: int = DEFAULT_API_TIMEOUT,
api_key: Optional[str] = None,
) -> None:
"""Initialize API client."""
self.session = session
@@ -48,6 +49,9 @@ class APIClient:
self.model = model
self.api_timeout = api_timeout
self.timeout = ClientTimeout(total=api_timeout)
self._api_key = api_key
if self.api_provider == API_PROVIDER_GEMINI and not api_key:
raise ValueError("Gemini provider requires api_key parameter")
self._closed = False
async def __aenter__(self):
@@ -119,7 +123,8 @@ class APIClient:
raise HomeAssistantError("API rate limit exceeded")
# Client/server errors — don't retry
_LOGGER.error("API error (status %d): %s", response.status, error_data)
truncated_error = str(error_data)[:512]
_LOGGER.error("API error (status %d): %s", response.status, truncated_error)
raise HomeAssistantError(f"API error: status {response.status}")
except asyncio.TimeoutError:
@@ -368,8 +373,7 @@ class APIClient:
genai = await asyncio.to_thread(import_genai)
# Extract API key from headers (Bearer token)
api_key = self.headers.get("Authorization", "").replace("Bearer ", "")
api_key = self._api_key
def create_client():
if self.endpoint and self.endpoint != "https://generativelanguage.googleapis.com/v1beta":
@@ -502,11 +506,11 @@ class APIClient:
}
except ImportError as e:
_LOGGER.error(f"Google Gemini library not installed: {str(e)}")
raise HomeAssistantError(f"Missing dependency: {str(e)}. Please install google-genai.")
_LOGGER.error("Google Gemini library not installed: %s", e)
raise HomeAssistantError("Missing dependency: google-genai. Please install it.")
except Exception as e:
_LOGGER.error(f"Gemini API error: {str(e)}")
raise HomeAssistantError(f"Gemini API error: {str(e)}")
_LOGGER.error("Gemini API error: %s", e)
raise HomeAssistantError("Gemini API request failed")
async def shutdown(self) -> None:
"""Shutdown API client."""