feat: implement MC domain filter proxy, API, dashboard

- proxy: asyncio TCP proxy with handshake parser, domain whitelist,
  transparent backend tunneling, SQLite logging, mtime hot reload
- api: FastAPI routes for config/domains/logs/status + restart trigger
- frontend: React + Vite NPM-style dashboard (dashboard/domains/logs/settings)
- nginx: reverse proxy for /api -> api:8000 and / -> frontend:3000
- docker-compose: full stack with shared data volume
- replace spec mc-domain-filter.md with README.md
This commit is contained in:
2026-05-20 16:39:18 +09:00
parent b45e884633
commit d10dae5cb9
33 changed files with 1872 additions and 223 deletions

58
proxy/config.py Normal file
View File

@@ -0,0 +1,58 @@
"""프록시용 config 로더.
API 서비스와 동일한 `data/config.json` 파일을 공유 볼륨으로 읽는다.
atomic rename(tempfile + os.replace) 으로 갱신되기 때문에 mtime polling
방식으로 안전하게 hot reload 가 가능하다.
"""
from __future__ import annotations
import json
import os
import threading
from pathlib import Path
CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/data/config.json"))
DEFAULT_CONFIG = {
"proxy": {"listen_port": 25565, "enabled": True},
"backend": {"host": "127.0.0.1", "port": 25565},
"allowed_domains": [
{"domain": "mc.tkrmagid.kr", "enabled": True, "note": "메인 도메인"}
],
}
_lock = threading.Lock()
def load() -> dict:
if not CONFIG_PATH.exists():
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
save(DEFAULT_CONFIG)
return dict(DEFAULT_CONFIG)
with _lock:
with CONFIG_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
def save(cfg: dict) -> None:
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp = CONFIG_PATH.with_suffix(".json.tmp")
with _lock:
with tmp.open("w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
os.replace(tmp, CONFIG_PATH)
def mtime() -> float:
try:
return CONFIG_PATH.stat().st_mtime
except FileNotFoundError:
return 0.0
def allowed_domain_set(cfg: dict) -> set[str]:
return {
d["domain"].lower().strip()
for d in cfg.get("allowed_domains", [])
if d.get("enabled", True)
}