iOSBuildServer/backend/config.py
2026-07-20 14:34:18 +09:00

168 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""项目配置 — 从环境变量读取,支持 .env 文件"""
import os
from pathlib import Path
from urllib.parse import quote, urlsplit, urlunsplit
# 自动加载 .env 文件(如果存在)
_env_file = Path(__file__).parent.parent / ".env"
if _env_file.exists():
with open(_env_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
value = value[1:-1]
os.environ.setdefault(key.strip(), value)
# 服务项目根目录
SERVER_ROOT = Path(__file__).resolve().parent.parent
def _resolve_server_path(value: str) -> Path:
"""将 .env 中的相对目录固定解析到服务项目根目录。"""
path = Path(value).expanduser()
if not path.is_absolute():
path = SERVER_ROOT / path
return path.resolve()
# 项目根目录下的初始化配置文件
BOOTSTRAP_CONFIG_PATH = SERVER_ROOT / "config.json"
# 自动打包服务自带的脚本和默认皮肤,不依赖分支源码中的 AutoPacking。
AUTOMATION_DIR = Path(__file__).resolve().parent / "automation"
# 持久化数据目录(不随分支变化,存放运行时配置和皮肤包)
DATA_DIR = _resolve_server_path(os.getenv("DATA_DIR", str(Path(__file__).parent / "data")))
DATA_DIR.mkdir(parents=True, exist_ok=True)
# config.json 路径(持久化,不随分支变化)
CONFIG_JSON_PATH = DATA_DIR / "config.json"
# 皮肤包存储目录(持久化,不随分支变化)
SKINS_DIR = DATA_DIR / "skins"
# 打包基础目录
BUILD_BASE_DIR = _resolve_server_path(os.getenv("BUILD_BASE_DIR", str(SERVER_ROOT / "build")))
# Git 源码目录
# 配置 GIT_REMOTE_URL 时,所有分支共用 GIT_SOURCE_DIR构建前会串行切换分支并复制快照。
# 未配置远程仓库时,继续兼容 GIT_SOURCE_BASE/<branch> 的人工维护目录。
GIT_SOURCE_BASE = _resolve_server_path(os.getenv("GIT_SOURCE_BASE", str(SERVER_ROOT / "ReadoorBranches")))
GIT_SOURCE_DIR = _resolve_server_path(os.getenv("GIT_SOURCE_DIR", str(GIT_SOURCE_BASE / "workspace")))
GIT_REMOTE_URL = os.getenv("GIT_REMOTE_URL", "")
GIT_USERNAME = os.getenv("GIT_USERNAME", "")
GIT_PASSWORD = os.getenv("GIT_PASSWORD", "")
def get_git_remote_url(with_credentials: bool = False) -> str:
"""获取 Git 远程地址,可选注入用户名密码"""
if not with_credentials or not GIT_REMOTE_URL or not GIT_USERNAME:
return GIT_REMOTE_URL
if "://" not in GIT_REMOTE_URL:
return GIT_REMOTE_URL
parts = urlsplit(GIT_REMOTE_URL)
if "@" in parts.netloc:
return GIT_REMOTE_URL
username = quote(GIT_USERNAME, safe="")
if GIT_PASSWORD:
password = quote(GIT_PASSWORD, safe="")
auth = f"{username}:{password}"
else:
auth = username
netloc = f"{auth}@{parts.netloc}"
return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
def mask_git_remote_url(url: str) -> str:
"""对 Git 远程地址中的凭证做脱敏"""
if not url or "://" not in url:
return url
parts = urlsplit(url)
if "@" not in parts.netloc:
return url
_, host = parts.netloc.rsplit("@", 1)
return urlunsplit((parts.scheme, f"***:***@{host}", parts.path, parts.query, parts.fragment))
def get_source_dir(branch: str) -> Path:
"""获取人工维护模式下指定分支的源码目录。"""
return GIT_SOURCE_BASE / branch
def get_shared_source_dir() -> Path:
"""获取配置远程仓库时使用的唯一共享源码工作目录。"""
return GIT_SOURCE_DIR
# 需要拷贝的目录和文件
COPY_ITEMS = [
"readoor",
"readoor.xcodeproj",
"readoorTests",
"Vendor",
"Podfile",
"Pods",
"Podfile.lock",
"readoor.xcworkspace",
]
# 服务端口
BACKEND_PORT = int(os.getenv("BACKEND_PORT", "8000"))
# 公网部署安全配置。开发环境保持低门槛,生产环境会在启动时强制校验关键项。
APP_ENV = os.getenv("APP_ENV", "development").lower()
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in os.getenv("CORS_ALLOWED_ORIGINS", "").split(",") if origin.strip()]
TRUSTED_HOSTS = [host.strip() for host in os.getenv("TRUSTED_HOSTS", "localhost,127.0.0.1,testserver").split(",") if host.strip()]
LOGIN_RATE_LIMIT_MAX_ATTEMPTS = int(os.getenv("LOGIN_RATE_LIMIT_MAX_ATTEMPTS", "5"))
LOGIN_RATE_LIMIT_WINDOW_SECONDS = int(os.getenv("LOGIN_RATE_LIMIT_WINDOW_SECONDS", "300"))
LOGIN_RATE_LIMIT_LOCKOUT_SECONDS = int(os.getenv("LOGIN_RATE_LIMIT_LOCKOUT_SECONDS", "900"))
# 数据库路径。测试可通过 DATABASE_URL 注入临时 SQLite避免影响运行库。
DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{Path(__file__).parent / 'build_server.db'}")
# WebSocket 日志队列最大长度
LOG_QUEUE_MAX_SIZE = 1000
# 默认最大并行打包数
DEFAULT_MAX_CONCURRENT_BUILDS = int(os.getenv("MAX_CONCURRENT_BUILDS", "2"))
# 默认打包目录保留时间(小时)
DEFAULT_BUILD_DIR_RETENTION_HOURS = int(os.getenv("BUILD_DIR_RETENTION_HOURS", "24"))
# 打包超时时间(小时)
BUILD_TIMEOUT_HOURS = int(os.getenv("BUILD_TIMEOUT_HOURS", "1"))
# 管理员账号配置
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
# JWT 配置
JWT_SECRET = os.getenv("JWT_SECRET", "ios-build-server-secret-key-change-in-production")
JWT_ALGORITHM = "HS256"
JWT_EXPIRE_HOURS = 24
def validate_production_security() -> None:
"""阻止带默认凭据或无访问边界的生产服务启动。"""
if APP_ENV != "production":
return
errors = []
if (JWT_SECRET == "ios-build-server-secret-key-change-in-production"
or JWT_SECRET.startswith("replace-") or len(JWT_SECRET) < 32):
errors.append("JWT_SECRET 必须设置为至少 32 位的随机字符串")
if (ADMIN_PASSWORD == "admin123" or ADMIN_PASSWORD.startswith("change-")
or len(ADMIN_PASSWORD) < 12):
errors.append("ADMIN_PASSWORD 必须设置为至少 12 位的强密码")
if not CORS_ALLOWED_ORIGINS or "*" in CORS_ALLOWED_ORIGINS:
errors.append("CORS_ALLOWED_ORIGINS 必须设置为实际 HTTPS 前端域名,且不能为 *")
if not os.getenv("TRUSTED_HOSTS") or "*" in TRUSTED_HOSTS:
errors.append("TRUSTED_HOSTS 必须设置为实际服务域名")
if errors:
raise RuntimeError("生产环境安全配置不完整: " + "; ".join(errors))