Build 0202261541: allow FQDN service URLs
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import ipaddress
|
||||
import os
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
|
||||
|
||||
@@ -64,6 +66,16 @@ SENSITIVE_KEYS = {
|
||||
"qbittorrent_password",
|
||||
}
|
||||
|
||||
URL_SETTING_KEYS = {
|
||||
"jellyseerr_base_url",
|
||||
"jellyfin_base_url",
|
||||
"jellyfin_public_url",
|
||||
"sonarr_base_url",
|
||||
"radarr_base_url",
|
||||
"prowlarr_base_url",
|
||||
"qbittorrent_base_url",
|
||||
}
|
||||
|
||||
SETTING_KEYS: List[str] = [
|
||||
"jellyseerr_base_url",
|
||||
"jellyseerr_api_key",
|
||||
@@ -107,6 +119,49 @@ def _normalize_username(value: str) -> str:
|
||||
normalized = normalized.split("@", 1)[0]
|
||||
return normalized
|
||||
|
||||
|
||||
def _is_ip_host(host: str) -> bool:
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_service_url(value: str) -> str:
|
||||
raw = value.strip()
|
||||
if not raw:
|
||||
raise ValueError("URL cannot be empty.")
|
||||
|
||||
candidate = raw
|
||||
if "://" not in candidate:
|
||||
authority = candidate.split("/", 1)[0].strip()
|
||||
if authority.startswith("["):
|
||||
closing = authority.find("]")
|
||||
host = authority[1:closing] if closing > 0 else authority.strip("[]")
|
||||
else:
|
||||
host = authority.split(":", 1)[0]
|
||||
host = host.strip().lower()
|
||||
default_scheme = "http" if host in {"localhost"} or _is_ip_host(host) or "." not in host else "https"
|
||||
candidate = f"{default_scheme}://{candidate}"
|
||||
|
||||
parsed = urlparse(candidate)
|
||||
if parsed.scheme not in {"http", "https"}:
|
||||
raise ValueError("URL must use http:// or https://.")
|
||||
if not parsed.netloc:
|
||||
raise ValueError("URL must include a host.")
|
||||
if parsed.query or parsed.fragment:
|
||||
raise ValueError("URL must not include query params or fragments.")
|
||||
if not parsed.hostname:
|
||||
raise ValueError("URL must include a valid host.")
|
||||
|
||||
normalized_path = parsed.path.rstrip("/")
|
||||
normalized = parsed._replace(path=normalized_path, params="", query="", fragment="")
|
||||
result = urlunparse(normalized).rstrip("/")
|
||||
if not result:
|
||||
raise ValueError("URL is invalid.")
|
||||
return result
|
||||
|
||||
def _normalize_root_folders(folders: Any) -> List[Dict[str, Any]]:
|
||||
if not isinstance(folders, list):
|
||||
return []
|
||||
@@ -203,7 +258,14 @@ async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
delete_setting(key)
|
||||
updates += 1
|
||||
continue
|
||||
set_setting(key, str(value))
|
||||
value_to_store = str(value).strip() if isinstance(value, str) else str(value)
|
||||
if key in URL_SETTING_KEYS and value_to_store:
|
||||
try:
|
||||
value_to_store = _normalize_service_url(value_to_store)
|
||||
except ValueError as exc:
|
||||
friendly_key = key.replace("_", " ")
|
||||
raise HTTPException(status_code=400, detail=f"{friendly_key}: {exc}") from exc
|
||||
set_setting(key, value_to_store)
|
||||
updates += 1
|
||||
if key in {"log_level", "log_file"}:
|
||||
touched_logging = True
|
||||
|
||||
Reference in New Issue
Block a user