Fix a setup issue with invalid hostnames

This commit is contained in:
Alex O'Connell
2025-10-04 11:37:52 -04:00
parent 78fe539078
commit a508a53d37
5 changed files with 72 additions and 30 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ main.log
*.xlsx
notes.txt
runpod_bootstrap.sh
*.code-workspace

View File

@@ -59,7 +59,7 @@
- [x] generic openai responses backend
- [ ] fix and re-upload all compatible old models (+ upload all original safetensors)
- [x] config entry migration function
- [ ] re-write setup guide
- [x] re-write setup guide
## more complicated ideas
- [ ] "context requests"

View File

@@ -39,7 +39,7 @@ from homeassistant.helpers.selector import (
BooleanSelectorConfig,
)
from .utils import download_model_from_hf, get_llama_cpp_python_version, install_llama_cpp_python, format_url, MissingQuantizationException
from .utils import download_model_from_hf, get_llama_cpp_python_version, install_llama_cpp_python, is_valid_hostname, MissingQuantizationException
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
@@ -324,35 +324,32 @@ class ConfigFlow(BaseConfigFlow, domain=DOMAIN):
if user_input:
self.client_config.update(user_input)
# validate remote connections
connect_err = await BACKEND_TO_CLS[self.client_config[CONF_BACKEND_TYPE]].async_validate_connection(self.hass, self.client_config)
if not connect_err:
return await self.async_step_finish()
hostname = user_input.get(CONF_HOST, "")
if not is_valid_hostname(hostname):
errors["base"] = "invalid_hostname"
else:
errors["base"] = "failed_to_connect"
description_placeholders["exception"] = str(connect_err)
return self.async_show_form(
step_id="user",
data_schema=remote_connection_schema(
self.client_config[CONF_BACKEND_TYPE],
host=user_input.get(CONF_HOST),
port=user_input.get(CONF_PORT),
ssl=user_input.get(CONF_SSL),
selected_path=user_input.get(CONF_GENERIC_OPENAI_PATH)
),
errors=errors,
description_placeholders=description_placeholders,
last_step=True
)
else:
return self.async_show_form(
step_id="user", data_schema=remote_connection_schema(self.client_config[CONF_BACKEND_TYPE],
host=self.client_config.get(CONF_HOST),
port=self.client_config.get(CONF_PORT),
ssl=self.client_config.get(CONF_SSL),
selected_path=self.client_config.get(CONF_GENERIC_OPENAI_PATH)
), last_step=True)
# validate remote connections
connect_err = await BACKEND_TO_CLS[self.client_config[CONF_BACKEND_TYPE]].async_validate_connection(self.hass, self.client_config)
if connect_err:
errors["base"] = "failed_to_connect"
description_placeholders["exception"] = str(connect_err)
else:
return await self.async_step_finish()
return self.async_show_form(
step_id="user",
data_schema=remote_connection_schema(
self.client_config[CONF_BACKEND_TYPE],
host=self.client_config.get(CONF_HOST),
port=self.client_config.get(CONF_PORT),
ssl=self.client_config.get(CONF_SSL),
selected_path=self.client_config.get(CONF_GENERIC_OPENAI_PATH)
),
errors=errors,
description_placeholders=description_placeholders,
last_step=True
)
else:
raise AbortFlow("Unknown internal step")

View File

@@ -2,6 +2,7 @@
"config": {
"error": {
"failed_to_connect": "Failed to connect to the remote API: {exception}",
"invalid_hostname": "The provided hostname was invalid. Please ensure you only provide the domain or IP address and not the full API endpoint.",
"unknown": "Unexpected error",
"pip_wheel_error": "Pip returned an error while installing the wheel! Please check the Home Assistant logs for more details."
},
@@ -191,6 +192,7 @@
},
"error": {
"failed_to_connect": "Failed to connect to the remote API: {exception}",
"invalid_hostname": "The provided hostname was invalid. Please ensure you only provide the domain or IP address and not the full API endpoint.",
"unknown": "Unexpected error"
}
},

View File

@@ -1,6 +1,7 @@
import time
import os
import re
import ipaddress
import sys
import platform
import logging
@@ -449,3 +450,44 @@ def parse_raw_tool_call(raw_block: str | dict, llm_api: llm.APIInstance, user_in
)
return tool_input, to_say
def is_valid_hostname(host: str) -> bool:
"""
Validates whether a string is a valid hostname or IP address,
rejecting URLs, paths, ports, query strings, etc.
"""
if not host or not isinstance(host, str):
return False
# Normalize: strip whitespace
host = host.strip().lower()
# Special case: localhost
if host == "localhost":
return True
# Try to parse as IPv4
try:
ipaddress.IPv4Address(host)
return True
except ipaddress.AddressValueError:
pass
# Try to parse as IPv6
try:
ipaddress.IPv6Address(host)
return True
except ipaddress.AddressValueError:
pass
# Validate as domain name (RFC 1034/1123)
# Rules:
# - Only a-z, 0-9, hyphens
# - No leading/trailing hyphens
# - Max 63 chars per label
# - At least 2 chars in TLD
# - No consecutive dots
domain_pattern = re.compile(r"^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?)*\.[a-z]{2,}$")
return bool(domain_pattern.match(host))