mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 07:03:58 +08:00
fix: Coordinator split, Gemini chat context, review findings fixes
- Extract HistoryManager (history.py) and MetricsManager (metrics.py) from coordinator
- Fix Gemini chat: use client.chats.create(history=...) instead of sequential send_message
- Fix float("inf") in metrics breaking json.dump persistence
- Fix _get_current_state checking wrong error key ("error" vs "error_message")
- Fix API key re-entry requirement when endpoint/provider changes in options flow
- Make CONF_API_KEY optional in options schema (stored key used as fallback)
- Add DNS rebinding protection via socket.getaddrinfo in validate_endpoint
- Remove mass assignment vulnerability in config_flow._create_entry
- Add description_placeholders to all error re-show paths
- Fix history migration overwriting existing JSON data
- Return list copy from get_limited_history to prevent reference leaks
- Add history_info to _get_safe_initial_state for data shape consistency
- Add defensive None check in _get_sanitized_last_response
- Move dt_util import to module level in config_flow
- Remove dead fallback branches in coordinator.last_response property
- Fix sensor min_latency display after float("inf") removal
- Fix history directory permissions from 0o777 to 0o755
- Deduplicate schema definitions via _build_provider_schema()
- Use centralized build_auth_headers from providers.py
This commit is contained in:
@@ -7,6 +7,7 @@ Utility functions for HA Text AI integration.
|
||||
@source: https://github.com/smkrv/ha-text-ai
|
||||
"""
|
||||
import ipaddress
|
||||
import socket
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -47,7 +48,7 @@ def validate_endpoint(endpoint: str) -> str:
|
||||
if not hostname:
|
||||
raise ValueError("Invalid endpoint URL: no hostname")
|
||||
|
||||
# Block private/reserved IPs
|
||||
# Block private/reserved IPs (direct IP or resolved hostname)
|
||||
try:
|
||||
addr = ipaddress.ip_address(hostname)
|
||||
if addr.is_private or addr.is_reserved or addr.is_loopback or addr.is_link_local:
|
||||
@@ -55,6 +56,23 @@ def validate_endpoint(endpoint: str) -> str:
|
||||
except ValueError as e:
|
||||
if "not allowed" in str(e):
|
||||
raise
|
||||
# Not an IP address — it's a hostname, which is fine
|
||||
# Not an IP literal — resolve hostname and check all resolved IPs
|
||||
# to prevent DNS rebinding attacks
|
||||
try:
|
||||
addrinfos = socket.getaddrinfo(hostname, None)
|
||||
for family, _type, _proto, _canonname, sockaddr in addrinfos:
|
||||
ip_str = sockaddr[0]
|
||||
resolved_addr = ipaddress.ip_address(ip_str)
|
||||
if (
|
||||
resolved_addr.is_private
|
||||
or resolved_addr.is_reserved
|
||||
or resolved_addr.is_loopback
|
||||
or resolved_addr.is_link_local
|
||||
):
|
||||
raise ValueError(
|
||||
f"Hostname {hostname} resolves to private/reserved IP {ip_str}"
|
||||
)
|
||||
except socket.gaierror:
|
||||
raise ValueError(f"Cannot resolve hostname: {hostname}")
|
||||
|
||||
return endpoint.rstrip("/")
|
||||
|
||||
Reference in New Issue
Block a user