auth: ROUTER_HOST как список + диагностика причин в логе

На Keenetic админка часто не слушает loopback — биндится только на
LAN-интерфейсах. Делаем ROUTER_HOST списком (через запятую), пробуем
все по порядку.

- auth.py: keenetic_validate_any() — обходит список, на каждом шаге
  пишет в лог конкретную причину отказа: unreachable / нет realm /
  POST HTTP <code> / неверный логин-пароль
- app.py: парсит ROUTER_HOST как список (csv); login fail логируется
- install.sh: при создании свежего .env подставляет в ROUTER_HOST
  обнаруженный LAN-IP, my.keenetic.net и loopback (через запятую)
- .env.example: тот же дефолт, комментарии обновлены
This commit is contained in:
Андрей Бобырев
2026-04-26 01:32:34 +03:00
parent 282f205759
commit d8ee078ef5
4 changed files with 56 additions and 16 deletions

17
app.py
View File

@@ -10,7 +10,7 @@ from pathlib import Path
from flask import Flask, Response, jsonify, request
from auth import is_token_valid, issue_token, keenetic_validate, revoke_token
from auth import is_token_valid, issue_token, keenetic_validate_any, revoke_token
from brand import inject_brand
from executor import run_items
from store import load_store, new_item, save_store
@@ -26,7 +26,8 @@ ALLOWED_IPS_RAW = os.environ.get("ALLOWED_IPS", "").strip()
ALLOWED_IPS = {x.strip() for x in ALLOWED_IPS_RAW.split(",") if x.strip()} if ALLOWED_IPS_RAW else set()
CMD_TIMEOUT = int(os.environ.get("CMD_TIMEOUT", "300"))
ROUTER_HOST = os.environ.get("ROUTER_HOST", "http://127.0.0.1").strip()
ROUTER_HOST_RAW = os.environ.get("ROUTER_HOST", "http://127.0.0.1").strip()
ROUTER_HOSTS = [h.strip() for h in ROUTER_HOST_RAW.split(",") if h.strip()]
ROUTER_LOGIN_DEFAULT = os.environ.get("ROUTER_LOGIN", "admin").strip() or "admin"
ROUTER_TIMEOUT = float(os.environ.get("ROUTER_TIMEOUT", "5") or "5")
@@ -64,13 +65,14 @@ def _require():
def _validate_credentials(login: str, password: str) -> bool:
if ROUTER_HOST:
if ROUTER_HOSTS:
try:
if keenetic_validate(ROUTER_HOST, login, password, timeout=ROUTER_TIMEOUT):
if keenetic_validate_any(ROUTER_HOSTS, login, password, timeout=ROUTER_TIMEOUT):
return True
except Exception:
log.exception("router auth call failed")
if WEB_PASSWORD and password == WEB_PASSWORD:
log.info("auth: вход по WEB_PASSWORD (мастер-пароль)")
return True
return False
@@ -93,12 +95,12 @@ def create_app() -> Flask:
if not _ip_allowed():
return jsonify({"ok": False, "error": "IP не разрешён"}), 403
info = {
"router_auth": bool(ROUTER_HOST),
"router_host": ROUTER_HOST,
"router_auth": bool(ROUTER_HOSTS),
"router_hosts": ROUTER_HOSTS,
"default_login": ROUTER_LOGIN_DEFAULT,
"password_fallback": bool(WEB_PASSWORD),
}
if not ROUTER_HOST and not WEB_PASSWORD:
if not ROUTER_HOSTS and not WEB_PASSWORD:
return jsonify({"ok": False, "error": "Задайте ROUTER_HOST или WEB_PASSWORD в .env", **info}), 503
return jsonify({"ok": _auth_ok(), **info})
@@ -112,6 +114,7 @@ def create_app() -> Flask:
if not password:
return jsonify({"ok": False, "error": "Пустой пароль"}), 400
if not _validate_credentials(login, password):
log.warning("login fail: ip=%s login=%s router_hosts=%s", _client_ip(), login, ROUTER_HOSTS)
return jsonify({"ok": False, "error": "Неверный логин или пароль"}), 401
token = issue_token()
log.info("login ok: ip=%s login=%s", _client_ip(), login)