Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions omlx/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2279,6 +2279,17 @@ async def update_global_settings(

# Apply server settings
if request.host is not None:
from ..utils.network import is_valid_bind_host

parts = [h.strip() for h in request.host.split(",") if h.strip()]
if not parts:
raise HTTPException(status_code=400, detail="Host cannot be empty")
for part in parts:
if not is_valid_bind_host(part):
raise HTTPException(
status_code=400,
detail=f"Invalid host: {part!r} (must be a hostname or IP address)",
)
global_settings.server.host = request.host
if request.port is not None:
global_settings.server.port = request.port
Expand Down
4 changes: 2 additions & 2 deletions omlx/admin/static/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@
window.location.href = '/admin';
} else {
const data = await response.json();
this.saveError = Array.isArray(data.detail) ? data.detail.join(', ') : (data.detail || window.t('js.error.save_settings_failed'));
this.saveError = Array.isArray(data.detail) ? data.detail.map(e => (e && typeof e === 'object') ? (e.msg || JSON.stringify(e)) : String(e)).join(', ') : (data.detail || window.t('js.error.save_settings_failed'));
// Reload settings to revert to server values
await this.loadGlobalSettings();
}
Expand Down Expand Up @@ -3201,7 +3201,7 @@
window.location.href = '/admin';
} else {
const data = await response.json();
alert(Array.isArray(data.detail) ? data.detail.join(', ') : (data.detail || 'Failed to save'));
alert(Array.isArray(data.detail) ? data.detail.map(e => (e && typeof e === 'object') ? (e.msg || JSON.stringify(e)) : String(e)).join(', ') : (data.detail || 'Failed to save'));
}
} catch (err) {
console.error('Failed to save HF mirror endpoint:', err);
Expand Down
6 changes: 4 additions & 2 deletions omlx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,16 @@ def serve_command(args):
)

# Start server
print(f"Starting server at http://{settings.server.host}:{settings.server.port}")
hosts = [h.strip() for h in settings.server.host.split(",") if h.strip()]
for h in hosts:
print(f"Starting server at http://{h}:{settings.server.port}")
# uvicorn does not support "trace" — map to "debug" for its internal logging
uvicorn_level = "debug" if settings.server.log_level == "trace" else settings.server.log_level
# Only show access logs at trace level
show_access_log = settings.server.log_level == "trace"
uvicorn.run(
app,
host=settings.server.host,
host=hosts if len(hosts) > 1 else hosts[0],
port=settings.server.port,
log_level=uvicorn_level,
access_log=show_access_log,
Expand Down
3 changes: 2 additions & 1 deletion omlx/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ def to_dict(self) -> dict[str, Any]:
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ServerSettings:
"""Create from dictionary."""
_host = data.get("host", "127.0.0.1")
return cls(
host=data.get("host", "127.0.0.1"),
host=", ".join(_host) if isinstance(_host, list) else str(_host),
port=data.get("port", 8000),
log_level=data.get("log_level", "info"),
cors_origins=data.get("cors_origins", ["*"]),
Expand Down
19 changes: 19 additions & 0 deletions omlx/utils/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ def is_valid_alias(value: str) -> bool:
return is_valid_ip(value)


def is_valid_bind_host(value: str) -> bool:
"""Return True if ``value`` is a valid host to bind a server socket to.

Accepts any parseable IP address (including ``0.0.0.0`` and ``::``) and
valid DNS hostnames. Unlike :func:`is_valid_alias`, unspecified addresses
are allowed because they are legitimate bind targets.
"""
if not isinstance(value, str):
return False
value = value.strip()
if not value:
return False
try:
ipaddress.ip_address(value)
return True
except ValueError:
return is_valid_hostname(value)


def _local_ipv4_addresses() -> list[str]:
"""Best-effort enumeration of non-loopback IPv4 addresses.

Expand Down