|
|
@@ -18,6 +18,7 @@ import json
|
|
|
import re
|
|
|
import sys
|
|
|
from pathlib import Path
|
|
|
+import os
|
|
|
from threading import Lock
|
|
|
from typing import Optional, Tuple
|
|
|
|
|
|
@@ -26,8 +27,38 @@ import requests
|
|
|
from log_util import get_logger, log_request
|
|
|
|
|
|
|
|
|
+def get_app_dir() -> Path:
|
|
|
+ """Return the directory where runtime files should be read/written.
|
|
|
+
|
|
|
+ Logic:
|
|
|
+ - If running as a PyInstaller one-file bundle, prefer the directory of the
|
|
|
+ executable (Path(sys.executable).parent).
|
|
|
+ - Otherwise prefer the current working directory (Path.cwd()).
|
|
|
+ - This makes writes (logs, config, protector_list) appear next to the exe
|
|
|
+ when distributed.
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ if getattr(sys, "frozen", False):
|
|
|
+ return Path(sys.executable).resolve().parent
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+
|
|
|
+ try:
|
|
|
+ return Path.cwd()
|
|
|
+ except Exception:
|
|
|
+ return Path(__file__).resolve().parent
|
|
|
+
|
|
|
+
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
|
-CONFIG_PATH = ROOT / "config.json"
|
|
|
+CONFIG_PATH = get_app_dir() / "config.json"
|
|
|
+
|
|
|
+
|
|
|
+DEFAULT_PAYLOAD = {
|
|
|
+ "username": "xiaobai",
|
|
|
+ "passwd": "dc81b4427df07fd6b3ebcb05a7b34daf",
|
|
|
+ "pass": "c2FsdF8xMXhpYW9iYWku",
|
|
|
+ "remember_password": "",
|
|
|
+}
|
|
|
|
|
|
|
|
|
# --- simple thread-safe in-memory session state ---
|
|
|
@@ -57,10 +88,54 @@ def clear_sess_key() -> None:
|
|
|
|
|
|
# --- login/request helpers ---
|
|
|
def load_config() -> dict:
|
|
|
- if not CONFIG_PATH.exists():
|
|
|
- raise FileNotFoundError(f"配置文件未找到: {CONFIG_PATH}")
|
|
|
- with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
|
- return json.load(f)
|
|
|
+ """Load configuration.
|
|
|
+
|
|
|
+ If the runtime config (CONFIG_PATH) does not exist, attempt to create it by
|
|
|
+ copying the project's default `config.json` (ROOT / 'config.json') or using
|
|
|
+ reasonable defaults. The created file will be written next to the exe or in
|
|
|
+ the current working directory so that packaged exe creates files in its
|
|
|
+ directory.
|
|
|
+ """
|
|
|
+ # If config exists in runtime location, load it.
|
|
|
+ if CONFIG_PATH.exists():
|
|
|
+ with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
|
+ return json.load(f)
|
|
|
+
|
|
|
+ # Otherwise try to obtain a project default from source tree (useful during
|
|
|
+ # development and when shipping defaults inside the package).
|
|
|
+ project_default = ROOT / "config.json"
|
|
|
+ default_cfg = {
|
|
|
+ "base_url": "http://ip:port/",
|
|
|
+ "conn_threshold": 20,
|
|
|
+ "scan_interval": 60,
|
|
|
+ "test_prefix": "Test_",
|
|
|
+ "rule_ip_limit": 1000,
|
|
|
+ }
|
|
|
+ if project_default.exists():
|
|
|
+ try:
|
|
|
+ with open(project_default, "r", encoding="utf-8") as f:
|
|
|
+ default_cfg = json.load(f)
|
|
|
+ except Exception:
|
|
|
+ # ignore and fall back to embedded defaults
|
|
|
+ pass
|
|
|
+
|
|
|
+ # Ensure runtime directory exists and write config atomically
|
|
|
+ try:
|
|
|
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+ tmp = CONFIG_PATH.with_suffix(".tmp")
|
|
|
+ with open(tmp, "w", encoding="utf-8") as f:
|
|
|
+ json.dump(default_cfg, f, ensure_ascii=False, indent=2)
|
|
|
+ try:
|
|
|
+ tmp.replace(CONFIG_PATH)
|
|
|
+ except Exception:
|
|
|
+ # fallback rename
|
|
|
+ tmp.rename(CONFIG_PATH)
|
|
|
+ except Exception as e:
|
|
|
+ # If we cannot write, surface a FileNotFoundError to preserve original
|
|
|
+ # callers' behavior.
|
|
|
+ raise FileNotFoundError(f"无法创建配置文件 {CONFIG_PATH}: {e}")
|
|
|
+
|
|
|
+ return default_cfg
|
|
|
|
|
|
|
|
|
def get_base_url() -> str:
|
|
|
@@ -72,7 +147,6 @@ def get_base_url() -> str:
|
|
|
def get_host() -> str:
|
|
|
"""Return host:port (netloc) parsed from base_url.
|
|
|
|
|
|
- Example: 'http://8.210.76.176:65531/' -> '8.210.76.176:65531'
|
|
|
"""
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
@@ -81,16 +155,6 @@ def get_host() -> str:
|
|
|
return ""
|
|
|
parsed = urlparse(base)
|
|
|
return parsed.netloc
|
|
|
-
|
|
|
-
|
|
|
-DEFAULT_PAYLOAD = {
|
|
|
- "username": "xiaobai",
|
|
|
- "passwd": "dc81b4427df07fd6b3ebcb05a7b34daf",
|
|
|
- "pass": "c2FsdF8xMXhpYW9iYWku",
|
|
|
- "remember_password": "",
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
def _extract_sess_cookie_from_response(resp: requests.Response) -> Optional[str]:
|
|
|
"""Try to extract sess_key cookie string from response.
|
|
|
|
|
|
@@ -109,13 +173,36 @@ def _extract_sess_cookie_from_response(resp: requests.Response) -> Optional[str]
|
|
|
return None
|
|
|
|
|
|
|
|
|
-def login(payload: dict | None = None, timeout: int = 10) -> Tuple[requests.Response, Optional[str]]:
|
|
|
+def login(payload: Optional[dict] = None, timeout: int = 10) -> Tuple[requests.Response, Optional[str]]:
|
|
|
"""Send login POST. Returns (response, sess_cookie_or_none).
|
|
|
|
|
|
The function will also set the in-memory sess_key if it can be extracted.
|
|
|
"""
|
|
|
cfg = load_config()
|
|
|
base = cfg.get("base_url", "").rstrip("/")
|
|
|
+ # Validate base URL to avoid passing placeholders like "http://ip:port/" to requests
|
|
|
+ from urllib.parse import urlparse
|
|
|
+
|
|
|
+ parsed = urlparse(base)
|
|
|
+ # If base_url is a placeholder (intentionally invalid), do not raise an
|
|
|
+ # exception here. Return a lightweight dummy response so the app can
|
|
|
+ # continue (GUI can prompt the user to fix config.json on first run).
|
|
|
+ if not base or not parsed.scheme or not parsed.netloc or "ip:port" in base:
|
|
|
+ logger = get_logger("login_post")
|
|
|
+ logger.warning(
|
|
|
+ "base_url appears to be a placeholder; skipping network login. GUI can be used to set a real base_url."
|
|
|
+ )
|
|
|
+
|
|
|
+ class _DummyResp:
|
|
|
+ def __init__(self):
|
|
|
+ self.status_code = 0
|
|
|
+ self.text = "base_url placeholder - no network request performed"
|
|
|
+
|
|
|
+ def json(self):
|
|
|
+ raise ValueError("No JSON available")
|
|
|
+
|
|
|
+ return _DummyResp(), None
|
|
|
+
|
|
|
url = f"{base}/Action/login"
|
|
|
data = payload or DEFAULT_PAYLOAD
|
|
|
logger = get_logger("login_post")
|