"""Nexus — FastAPI Dependency Injection Wires Repository implementations → Service instances → API route handlers. All DI occurs here; routes never import infrastructure directly. D7 Fix: Service factories now read session from request.state.db (set by DbSessionMiddleware) instead of creating unclosed AsyncSessionLocal() instances. This fixes the P0 session leak bug where 4 service factories never closed sessions. """ from typing import AsyncGenerator from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession from server.infrastructure.database.session import AsyncSessionLocal from server.infrastructure.database.server_repo import ServerRepositoryImpl from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl from server.infrastructure.database.script_repo import ScriptRepositoryImpl, ScriptExecutionRepositoryImpl from server.infrastructure.database.db_credential_repo import DbCredentialRepositoryImpl from server.infrastructure.database.admin_repo import AdminRepositoryImpl, LoginAttemptRepositoryImpl from server.infrastructure.database.refresh_token_repo import RefreshTokenRepositoryImpl from server.infrastructure.database.setting_repo import SettingRepositoryImpl from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl from server.infrastructure.database.push_schedule_repo import PushScheduleRepositoryImpl, PushRetryJobRepositoryImpl from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl from server.application.services.server_service import ServerService from server.application.services.script_service import ScriptService from server.application.services.auth_service import AuthService from server.application.services.sync_service import SyncService async def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]: """Yield DB session from middleware (request.state.db). DbSessionMiddleware ensures every /api/ request has a session. This dependency simply yields it so routes can use Depends(get_db). No double-session: one session per request, shared everywhere. """ session = getattr(request.state, "db", None) if session is not None: yield session else: # Fallback for tests or edge cases without middleware session = AsyncSessionLocal() try: yield session finally: await session.close() async def _get_request_session(request: Request) -> AsyncSession: """Get DB session from middleware (request.state.db). DbSessionMiddleware opens a session at the start of each API request and stores it in request.state.db. This ensures: 1. Session is always properly closed after the request 2. All repositories/services in the same request share the same session 3. No more leaked sessions from service factories """ session = getattr(request.state, "db", None) if session is None: # Fallback: if middleware didn't set session (shouldn't happen for /api/ routes) raise RuntimeError("DB session not found in request.state — DbSessionMiddleware may be missing") return session # ── Repository Factories ── def _server_repo(session: AsyncSession) -> ServerRepositoryImpl: return ServerRepositoryImpl(session) def _sync_log_repo(session: AsyncSession) -> SyncLogRepositoryImpl: return SyncLogRepositoryImpl(session) def _script_repo(session: AsyncSession) -> ScriptRepositoryImpl: return ScriptRepositoryImpl(session) def _execution_repo(session: AsyncSession) -> ScriptExecutionRepositoryImpl: return ScriptExecutionRepositoryImpl(session) def _credential_repo(session: AsyncSession) -> DbCredentialRepositoryImpl: return DbCredentialRepositoryImpl(session) def _admin_repo(session: AsyncSession) -> AdminRepositoryImpl: return AdminRepositoryImpl(session) def _refresh_token_repo(session: AsyncSession) -> RefreshTokenRepositoryImpl: return RefreshTokenRepositoryImpl(session) def _attempt_repo(session: AsyncSession) -> LoginAttemptRepositoryImpl: return LoginAttemptRepositoryImpl(session) def _setting_repo(session: AsyncSession) -> SettingRepositoryImpl: return SettingRepositoryImpl(session) def _audit_repo(session: AsyncSession) -> AuditLogRepositoryImpl: return AuditLogRepositoryImpl(session) def _schedule_repo(session: AsyncSession) -> PushScheduleRepositoryImpl: return PushScheduleRepositoryImpl(session) def _retry_repo(session: AsyncSession) -> PushRetryJobRepositoryImpl: return PushRetryJobRepositoryImpl(session) def _preset_repo(session: AsyncSession) -> PasswordPresetRepositoryImpl: return PasswordPresetRepositoryImpl(session) # ── Service Factories (D7 fixed: use request.state.db) ── async def get_server_service(request: Request) -> ServerService: session = await _get_request_session(request) return ServerService( server_repo=_server_repo(session), sync_log_repo=_sync_log_repo(session), ) async def get_script_service(request: Request) -> ScriptService: session = await _get_request_session(request) return ScriptService( script_repo=_script_repo(session), execution_repo=_execution_repo(session), credential_repo=_credential_repo(session), server_repo=_server_repo(session), audit_repo=_audit_repo(session), ) async def get_auth_service(request: Request) -> AuthService: session = await _get_request_session(request) return AuthService( admin_repo=_admin_repo(session), attempt_repo=_attempt_repo(session), audit_repo=_audit_repo(session), refresh_token_repo=_refresh_token_repo(session), ) async def get_sync_service(request: Request) -> SyncService: session = await _get_request_session(request) return SyncService( server_repo=_server_repo(session), sync_log_repo=_sync_log_repo(session), audit_repo=_audit_repo(session), retry_repo=_retry_repo(session), ) # ── Command Safety Detection (P2-18) ── import re import logging _logger = logging.getLogger("nexus.command_safety") # Patterns that indicate highly dangerous commands _DANGEROUS_PATTERNS = [ (r"\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|-r[a-zA-Z]*\s+|.*--no-preserve-root\s+)/\s*$", "rm -rf / (全盘删除)"), (r"\bdd\s+.*of=/dev/[a-z]+\s", "dd 写入裸设备 (数据毁灭)"), (r":\(\)\{\s*:\|:&\s*\}", "Fork bomb (内存耗尽)"), (r"mkfs\.", "格式化文件系统"), (r">\s*/dev/sd[a-z]", "覆写磁盘设备"), (r"chmod\s+(-R\s+)?000\s+/", "根目录权限清零"), (r"chown\s+(-R\s+)?\S+\s+/", "根目录归属变更"), ] _DANGEROUS_COMPILED = [(re.compile(p, re.IGNORECASE), desc) for p, desc in _DANGEROUS_PATTERNS] def check_dangerous_command(command: str) -> list[str]: """Check a command for dangerous patterns. Returns a list of warning descriptions (empty if safe). This is a warning system, not a blocker — commands are still executed, but dangerous ones are logged at WARNING level for audit trail. """ warnings = [] for pattern, desc in _DANGEROUS_COMPILED: if pattern.search(command): warnings.append(desc) if warnings: _logger.warning(f"Dangerous command detected: {command[:200]} -> {warnings}") return warnings