auth_middleware.cpp 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /*
  2. * auth_middleware.cpp - Authentication middleware implementation
  3. */
  4. #include "auth_middleware.h"
  5. #include <cstdio>
  6. #include <cstring>
  7. #include <sstream>
  8. namespace auth {
  9. AuthMiddleware::AuthMiddleware() {}
  10. void AuthMiddleware::init(SessionManager *sessions, RateLimiter *limiter) {
  11. sessions_ = sessions;
  12. limiter_ = limiter;
  13. printf("[auth] AuthMiddleware initialized\n");
  14. }
  15. const std::set<std::string>& AuthMiddleware::public_routes() {
  16. static const std::set<std::string> routes = {
  17. // fix24 v27: 首页和监控数据API均需登录(从公开路由移除)
  18. // "/" — 首页需要登录
  19. // "/api/monitor", "/api/messages", "/api/connections" — 首页数据API需要登录
  20. "/login", "/api/auth/login", "/api/auth/logout", "/api/auth/status",
  21. // Video/photo streams (keep public for external monitoring)
  22. "/video", "/weight", "/photo",
  23. "/api/weight/records",
  24. "/api/sync/stats", "/api/sync/events",
  25. // ROI endpoints (read)
  26. "/api/roi/snapshot", "/api/roi/config",
  27. // Streaming
  28. "/api/stream", "/api/mjpeg",
  29. // fix24 v24: 管理页面和API均需登录认证
  30. // /system, /frpc, /ssh-keys, /logs 及其 API 均需登录后访问
  31. // Audit log (read, requires admin but checked at API level)
  32. "/api/auth/audit",
  33. // Static assets
  34. "/favicon.ico",
  35. };
  36. return routes;
  37. }
  38. bool AuthMiddleware::is_public_route(const std::string &path) {
  39. // Exact match
  40. if (public_routes().count(path)) return true;
  41. // Prefix match for static assets
  42. if (path.rfind("/assets/", 0) == 0) return true;
  43. if (path.rfind("/static/", 0) == 0) return true;
  44. return false;
  45. }
  46. std::string AuthMiddleware::extract_cookie(const httplib::Request &req,
  47. const std::string &name) {
  48. auto it = req.headers.find("Cookie");
  49. if (it == req.headers.end()) return "";
  50. std::string cookies = it->second;
  51. std::string search = name + "=";
  52. size_t pos = cookies.find(search);
  53. if (pos == std::string::npos) return "";
  54. pos += search.length();
  55. size_t end = cookies.find(';', pos);
  56. if (end == std::string::npos) end = cookies.length();
  57. return cookies.substr(pos, end - pos);
  58. }
  59. std::string AuthMiddleware::get_client_ip(const httplib::Request &req) {
  60. // Check X-Forwarded-For (for reverse proxy)
  61. auto it = req.headers.find("X-Forwarded-For");
  62. if (it != req.headers.end() && !it->second.empty()) {
  63. // Take first IP in the list
  64. size_t comma = it->second.find(',');
  65. if (comma != std::string::npos)
  66. return it->second.substr(0, comma);
  67. return it->second;
  68. }
  69. // Check X-Real-IP
  70. it = req.headers.find("X-Real-IP");
  71. if (it != req.headers.end() && !it->second.empty()) {
  72. return it->second;
  73. }
  74. // Fall back to remote address
  75. return req.remote_addr;
  76. }
  77. std::string AuthMiddleware::check_auth(const httplib::Request &req, Session &sess) {
  78. std::string session_id = extract_cookie(req, "session_id");
  79. if (session_id.empty()) return "";
  80. if (!sessions_) return "";
  81. if (sessions_->validate_session(session_id, sess)) {
  82. return session_id;
  83. }
  84. return "";
  85. }
  86. httplib::Server::HandlerWithResponse AuthMiddleware::get_pre_routing_handler() {
  87. return [this](const httplib::Request &req, httplib::Response &res)
  88. -> httplib::Server::HandlerResponse {
  89. // Skip auth for public routes
  90. if (is_public_route(req.path)) {
  91. return httplib::Server::HandlerResponse::Unhandled; // 继续路由
  92. }
  93. // Check authentication
  94. Session sess;
  95. std::string session_id = check_auth(req, sess);
  96. if (session_id.empty()) {
  97. // Check if this is an API request
  98. bool is_api = (req.path.rfind("/api/", 0) == 0);
  99. if (is_api) {
  100. res.status = 401;
  101. res.set_header("Content-Type", "application/json");
  102. res.body = "{\"error\":\"Unauthorized\",\"message\":\"Please login\"}";
  103. } else {
  104. // Redirect to login page
  105. res.status = 302;
  106. res.set_header("Location", "/login?redirect=" + req.path);
  107. }
  108. return httplib::Server::HandlerResponse::Handled; // 已处理,中断路由
  109. }
  110. // CSRF check for state-changing methods (POST/PUT/DELETE)
  111. if (req.method == "POST" || req.method == "PUT" || req.method == "DELETE") {
  112. // Get CSRF token from header or body
  113. std::string csrf_token;
  114. auto it = req.headers.find("X-CSRF-Token");
  115. if (it != req.headers.end()) {
  116. csrf_token = it->second;
  117. } else {
  118. // Check form body / query params
  119. csrf_token = req.get_param_value("csrf_token");
  120. }
  121. std::string expected_csrf = sessions_->get_csrf_token(session_id);
  122. if (!expected_csrf.empty() && csrf_token != expected_csrf) {
  123. res.status = 403;
  124. res.set_header("Content-Type", "application/json");
  125. res.body = "{\"error\":\"Forbidden\",\"message\":\"CSRF token invalid\"}";
  126. fprintf(stderr, "[auth] CSRF check failed for %s from %s\n",
  127. req.path.c_str(), get_client_ip(req).c_str());
  128. return httplib::Server::HandlerResponse::Handled; // 已处理,中断路由
  129. }
  130. }
  131. // Role-based access control (fix24 v28)
  132. // role=0: 普通用户 — 可访问首页/视频/称重/锁定管理
  133. // role=1: 管理员 — 可访问首页/视频/称重/锁定管理/配置
  134. // role=2: 超级管理员 — 所有页面(含帮助手册)
  135. int user_role = sess.user_role;
  136. // 超级管理员专属 (role >= 2)
  137. if (user_role < 2) {
  138. static const std::set<std::string> superadmin_exact = {
  139. "/system", "/frpc", "/ssh-keys", "/logs", "/help",
  140. };
  141. static const char* superadmin_prefixes[] = {
  142. "/api/frpc/", "/api/ssh/", "/api/logs/", "/api/monitor/",
  143. nullptr
  144. };
  145. bool blocked = superadmin_exact.count(req.path) > 0;
  146. if (!blocked) {
  147. for (int i = 0; superadmin_prefixes[i]; i++) {
  148. if (req.path.rfind(superadmin_prefixes[i], 0) == 0) {
  149. blocked = true;
  150. break;
  151. }
  152. }
  153. }
  154. if (blocked) {
  155. bool is_api = (req.path.rfind("/api/", 0) == 0);
  156. if (is_api) {
  157. res.status = 403;
  158. res.set_header("Content-Type", "application/json");
  159. res.body = "{\"error\":\"Forbidden\",\"message\":\"Requires superadmin\"}";
  160. } else {
  161. res.status = 302;
  162. res.set_header("Location", "/");
  163. }
  164. return httplib::Server::HandlerResponse::Handled;
  165. }
  166. }
  167. // 管理员专属 (role >= 1)
  168. if (user_role < 1) {
  169. static const std::set<std::string> admin_exact = {
  170. "/config",
  171. };
  172. static const char* admin_prefixes[] = {
  173. "/api/config/", "/api/locks/",
  174. nullptr
  175. };
  176. bool blocked = admin_exact.count(req.path) > 0;
  177. if (!blocked) {
  178. for (int i = 0; admin_prefixes[i]; i++) {
  179. if (req.path.rfind(admin_prefixes[i], 0) == 0) {
  180. blocked = true;
  181. break;
  182. }
  183. }
  184. }
  185. if (blocked) {
  186. bool is_api = (req.path.rfind("/api/", 0) == 0);
  187. if (is_api) {
  188. res.status = 403;
  189. res.set_header("Content-Type", "application/json");
  190. res.body = "{\"error\":\"Forbidden\",\"message\":\"Requires admin\"}";
  191. } else {
  192. res.status = 302;
  193. res.set_header("Location", "/");
  194. }
  195. return httplib::Server::HandlerResponse::Handled;
  196. }
  197. }
  198. return httplib::Server::HandlerResponse::Unhandled; // 认证通过,继续路由
  199. };
  200. }
  201. httplib::Server::HandlerWithResponse AuthMiddleware::get_post_routing_handler() {
  202. return [](const httplib::Request &req, httplib::Response &res) {
  203. // Security headers (所有响应)
  204. res.set_header("X-Content-Type-Options", "nosniff");
  205. res.set_header("X-Frame-Options", "DENY");
  206. res.set_header("X-XSS-Protection", "1; mode=block");
  207. res.set_header("Referrer-Policy", "strict-origin-when-cross-origin");
  208. // fix24 v35: 静态文件缓存优化
  209. // 对 /assets/ 路径下的静态文件设置缓存,减少浏览器重复请求
  210. bool is_static = (req.path.rfind("/assets/", 0) == 0 ||
  211. req.path.rfind("/static/", 0) == 0);
  212. if (is_static) {
  213. // 静态文件缓存1小时
  214. res.set_header("Cache-Control", "public, max-age=3600");
  215. res.set_header("Pragma", "");
  216. } else {
  217. // 动态页面和API不缓存
  218. res.set_header("Cache-Control", "no-store, no-cache, must-revalidate");
  219. res.set_header("Pragma", "no-cache");
  220. // CSP只在HTML页面设置(减少其他资源类型的开销)
  221. if (res.get_header_value("Content-Type").find("text/html") != std::string::npos) {
  222. res.set_header("Content-Security-Policy",
  223. "default-src 'self'; "
  224. "script-src 'self' 'unsafe-inline'; "
  225. "style-src 'self' 'unsafe-inline'; "
  226. "img-src 'self' data:; "
  227. "connect-src 'self' ws: wss:;");
  228. }
  229. }
  230. return httplib::Server::HandlerResponse::Handled;
  231. };
  232. }
  233. } // namespace auth