/* * auth_middleware.cpp - Authentication middleware implementation */ #include "auth_middleware.h" #include #include #include namespace auth { AuthMiddleware::AuthMiddleware() {} void AuthMiddleware::init(SessionManager *sessions, RateLimiter *limiter) { sessions_ = sessions; limiter_ = limiter; printf("[auth] AuthMiddleware initialized\n"); } const std::set& AuthMiddleware::public_routes() { static const std::set routes = { // fix24 v27: 首页和监控数据API均需登录(从公开路由移除) // "/" — 首页需要登录 // "/api/monitor", "/api/messages", "/api/connections" — 首页数据API需要登录 "/login", "/api/auth/login", "/api/auth/logout", "/api/auth/status", // Video/photo streams (keep public for external monitoring) "/video", "/weight", "/photo", "/api/weight/records", "/api/sync/stats", "/api/sync/events", // ROI endpoints (read) "/api/roi/snapshot", "/api/roi/config", // Streaming "/api/stream", "/api/mjpeg", // fix24 v24: 管理页面和API均需登录认证 // /system, /frpc, /ssh-keys, /logs 及其 API 均需登录后访问 // Audit log (read, requires admin but checked at API level) "/api/auth/audit", // Static assets "/favicon.ico", }; return routes; } bool AuthMiddleware::is_public_route(const std::string &path) { // Exact match if (public_routes().count(path)) return true; // Prefix match for static assets if (path.rfind("/assets/", 0) == 0) return true; if (path.rfind("/static/", 0) == 0) return true; return false; } std::string AuthMiddleware::extract_cookie(const httplib::Request &req, const std::string &name) { auto it = req.headers.find("Cookie"); if (it == req.headers.end()) return ""; std::string cookies = it->second; std::string search = name + "="; size_t pos = cookies.find(search); if (pos == std::string::npos) return ""; pos += search.length(); size_t end = cookies.find(';', pos); if (end == std::string::npos) end = cookies.length(); return cookies.substr(pos, end - pos); } std::string AuthMiddleware::get_client_ip(const httplib::Request &req) { // Check X-Forwarded-For (for reverse proxy) auto it = req.headers.find("X-Forwarded-For"); if (it != req.headers.end() && !it->second.empty()) { // Take first IP in the list size_t comma = it->second.find(','); if (comma != std::string::npos) return it->second.substr(0, comma); return it->second; } // Check X-Real-IP it = req.headers.find("X-Real-IP"); if (it != req.headers.end() && !it->second.empty()) { return it->second; } // Fall back to remote address return req.remote_addr; } std::string AuthMiddleware::check_auth(const httplib::Request &req, Session &sess) { std::string session_id = extract_cookie(req, "session_id"); if (session_id.empty()) return ""; if (!sessions_) return ""; if (sessions_->validate_session(session_id, sess)) { return session_id; } return ""; } httplib::Server::HandlerWithResponse AuthMiddleware::get_pre_routing_handler() { return [this](const httplib::Request &req, httplib::Response &res) -> httplib::Server::HandlerResponse { // Skip auth for public routes if (is_public_route(req.path)) { return httplib::Server::HandlerResponse::Unhandled; // 继续路由 } // Check authentication Session sess; std::string session_id = check_auth(req, sess); if (session_id.empty()) { // Check if this is an API request bool is_api = (req.path.rfind("/api/", 0) == 0); if (is_api) { res.status = 401; res.set_header("Content-Type", "application/json"); res.body = "{\"error\":\"Unauthorized\",\"message\":\"Please login\"}"; } else { // Redirect to login page res.status = 302; res.set_header("Location", "/login?redirect=" + req.path); } return httplib::Server::HandlerResponse::Handled; // 已处理,中断路由 } // CSRF check for state-changing methods (POST/PUT/DELETE) if (req.method == "POST" || req.method == "PUT" || req.method == "DELETE") { // Get CSRF token from header or body std::string csrf_token; auto it = req.headers.find("X-CSRF-Token"); if (it != req.headers.end()) { csrf_token = it->second; } else { // Check form body / query params csrf_token = req.get_param_value("csrf_token"); } std::string expected_csrf = sessions_->get_csrf_token(session_id); if (!expected_csrf.empty() && csrf_token != expected_csrf) { res.status = 403; res.set_header("Content-Type", "application/json"); res.body = "{\"error\":\"Forbidden\",\"message\":\"CSRF token invalid\"}"; fprintf(stderr, "[auth] CSRF check failed for %s from %s\n", req.path.c_str(), get_client_ip(req).c_str()); return httplib::Server::HandlerResponse::Handled; // 已处理,中断路由 } } // Role-based access control (fix24 v28) // role=0: 普通用户 — 可访问首页/视频/称重/锁定管理 // role=1: 管理员 — 可访问首页/视频/称重/锁定管理/配置 // role=2: 超级管理员 — 所有页面(含帮助手册) int user_role = sess.user_role; // 超级管理员专属 (role >= 2) if (user_role < 2) { static const std::set superadmin_exact = { "/system", "/frpc", "/ssh-keys", "/logs", "/help", }; static const char* superadmin_prefixes[] = { "/api/frpc/", "/api/ssh/", "/api/logs/", "/api/monitor/", nullptr }; bool blocked = superadmin_exact.count(req.path) > 0; if (!blocked) { for (int i = 0; superadmin_prefixes[i]; i++) { if (req.path.rfind(superadmin_prefixes[i], 0) == 0) { blocked = true; break; } } } if (blocked) { bool is_api = (req.path.rfind("/api/", 0) == 0); if (is_api) { res.status = 403; res.set_header("Content-Type", "application/json"); res.body = "{\"error\":\"Forbidden\",\"message\":\"Requires superadmin\"}"; } else { res.status = 302; res.set_header("Location", "/"); } return httplib::Server::HandlerResponse::Handled; } } // 管理员专属 (role >= 1) if (user_role < 1) { static const std::set admin_exact = { "/config", }; static const char* admin_prefixes[] = { "/api/config/", "/api/locks/", nullptr }; bool blocked = admin_exact.count(req.path) > 0; if (!blocked) { for (int i = 0; admin_prefixes[i]; i++) { if (req.path.rfind(admin_prefixes[i], 0) == 0) { blocked = true; break; } } } if (blocked) { bool is_api = (req.path.rfind("/api/", 0) == 0); if (is_api) { res.status = 403; res.set_header("Content-Type", "application/json"); res.body = "{\"error\":\"Forbidden\",\"message\":\"Requires admin\"}"; } else { res.status = 302; res.set_header("Location", "/"); } return httplib::Server::HandlerResponse::Handled; } } return httplib::Server::HandlerResponse::Unhandled; // 认证通过,继续路由 }; } httplib::Server::HandlerWithResponse AuthMiddleware::get_post_routing_handler() { return [](const httplib::Request &req, httplib::Response &res) { // Security headers (所有响应) res.set_header("X-Content-Type-Options", "nosniff"); res.set_header("X-Frame-Options", "DENY"); res.set_header("X-XSS-Protection", "1; mode=block"); res.set_header("Referrer-Policy", "strict-origin-when-cross-origin"); // fix24 v35: 静态文件缓存优化 // 对 /assets/ 路径下的静态文件设置缓存,减少浏览器重复请求 bool is_static = (req.path.rfind("/assets/", 0) == 0 || req.path.rfind("/static/", 0) == 0); if (is_static) { // 静态文件缓存1小时 res.set_header("Cache-Control", "public, max-age=3600"); res.set_header("Pragma", ""); } else { // 动态页面和API不缓存 res.set_header("Cache-Control", "no-store, no-cache, must-revalidate"); res.set_header("Pragma", "no-cache"); // CSP只在HTML页面设置(减少其他资源类型的开销) if (res.get_header_value("Content-Type").find("text/html") != std::string::npos) { res.set_header("Content-Security-Policy", "default-src 'self'; " "script-src 'self' 'unsafe-inline'; " "style-src 'self' 'unsafe-inline'; " "img-src 'self' data:; " "connect-src 'self' ws: wss:;"); } } return httplib::Server::HandlerResponse::Handled; }; } } // namespace auth