feat: normalize base_url and strip credentials from logs
New url_util.py (deliberately separate from the vendored files, so a future upstream re-sync never conflicts with it): normalize_base_url strips whitespace/trailing slashes and rejects non-http(s) or hostless URLs; sanitize_url_for_logging masks embedded basic-auth credentials for Task 7's discovery logging. Wires normalization into config_flow.validate_input before the client is built, with a new invalid_base_url form error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
This commit is contained in:
1 parent
564c52159f
commit
914c89dc6b
4 files changed
+86
No files matched your search
@@ -103,6 +103,7 @@ from .const import (
|
||||
UNSUPPORTED_PRIORITY_SERVICE_TIERS_MODELS,
|
||||
UNSUPPORTED_WEB_SEARCH_MODELS,
|
||||
)
|
||||
from .url_util import InvalidBaseURL, normalize_base_url
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -119,6 +120,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
data[CONF_BASE_URL] = normalize_base_url(data[CONF_BASE_URL])
|
||||
client = openai.AsyncOpenAI(
|
||||
api_key=data[CONF_API_KEY],
|
||||
base_url=data[CONF_BASE_URL],
|
||||
@@ -145,6 +147,8 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except InvalidBaseURL:
|
||||
errors["base"] = "invalid_base_url"
|
||||
except openai.APIConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except openai.AuthenticationError:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_base_url": "That does not look like a valid http(s) URL.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Base-URL normalization and safe logging.
|
||||
|
||||
This module is intentionally separate from the vendored upstream files so that
|
||||
re-syncing with home-assistant/core never conflicts with it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
ALLOWED_SCHEMES = frozenset({"http", "https"})
|
||||
|
||||
|
||||
class InvalidBaseURL(ValueError):
|
||||
"""Raised when a base URL is unusable."""
|
||||
|
||||
|
||||
def normalize_base_url(url: str) -> str:
|
||||
"""Strip whitespace and trailing slashes, and validate the scheme."""
|
||||
cleaned = (url or "").strip()
|
||||
if not cleaned:
|
||||
raise InvalidBaseURL("base URL is empty")
|
||||
|
||||
parsed = urlparse(cleaned)
|
||||
if parsed.scheme not in ALLOWED_SCHEMES:
|
||||
raise InvalidBaseURL(f"unsupported scheme: {parsed.scheme or '(none)'}")
|
||||
if not parsed.netloc:
|
||||
raise InvalidBaseURL("base URL has no host")
|
||||
|
||||
return urlunparse(parsed._replace(path=parsed.path.rstrip("/")))
|
||||
|
||||
|
||||
def sanitize_url_for_logging(url: str) -> str:
|
||||
"""Return the URL with any embedded credentials replaced by ***."""
|
||||
parsed = urlparse(url)
|
||||
if not parsed.username and not parsed.password:
|
||||
return url
|
||||
|
||||
host = parsed.hostname or ""
|
||||
if parsed.port:
|
||||
host = f"{host}:{parsed.port}"
|
||||
return urlunparse(parsed._replace(netloc=f"***@{host}"))
|
||||
Reference in new issue
Block a user