| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- /*
- * auth_middleware.cpp - Authentication middleware implementation
- */
- #include "auth_middleware.h"
- #include <cstdio>
- #include <cstring>
- #include <sstream>
- namespace auth {
- AuthMiddleware::AuthMiddleware() {}
- void AuthMiddleware::init(SessionManager *sessions, RateLimiter *limiter) {
- sessions_ = sessions;
- limiter_ = limiter;
- printf("[auth] AuthMiddleware initialized\n");
- }
- const std::set<std::string>& AuthMiddleware::public_routes() {
- static const std::set<std::string> 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<std::string> 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<std::string> 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
|