#include "ServiceLogic.h" #include "Core/Singleton.h" #include "Database.h" #include "HttpSession.h" #include "SessionStore.h" #include "Settings.h" #include namespace ServiceLogic { using namespace boost::beast; std::string extractToken(const std::string &cookieHeader, const std::string &tokenName = "access_token") { // 格式示例:"access_token=abc123; Path=/; Expires=Wed, 21 Oct 2023 07:28:00 GMT" size_t startPos = cookieHeader.find(tokenName + "="); if (startPos == std::string::npos) { return ""; } startPos += tokenName.size() + 1; // 跳过 "token_name=" size_t endPos = cookieHeader.find(';', startPos); if (endPos == std::string::npos) { endPos = cookieHeader.size(); } std::string token = cookieHeader.substr(startPos, endPos - startPos); // 移除可能的引号和空格 token.erase(std::remove(token.begin(), token.end(), '"'), token.end()); token.erase(std::remove(token.begin(), token.end(), ' '), token.end()); return token; } boost::beast::http::response serverError(const boost::beast::http::request &request, std::string_view errorMessage) { using namespace boost::beast; http::response res{http::status::internal_server_error, request.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, "text/html"); res.keep_alive(request.keep_alive()); std::ostringstream oss; oss << "An error occurred: '" << errorMessage << "'"; res.body() = oss.str(); res.prepare_payload(); return res; } http::response badRequest(const http::request &request, std::string_view why) { http::response res{http::status::bad_request, request.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, "text/html"); res.keep_alive(request.keep_alive()); res.body() = std::string(why); res.prepare_payload(); return res; } void staticFilesDeploy() { using namespace Core; using namespace boost::urls; auto application = Singleton::instance(); // clang-format off application->insertUrl("/{path*}", [](Older::HttpSession &session, const Older::Application::Request &request, const matches &matches) { using namespace boost::beast; url_view view(request.target()); auto target = view.path(); LOG(info) << target; if (target.find("..") != boost::beast::string_view::npos) { session.reply(ServiceLogic::badRequest(request, "Illegal request-target")); return; } auto settings = Singleton::instance(); std::string path = ResponseUtility::pathCat(settings->documentRoot(), target); if (target.back() == '/') path.append("index.html"); if (std::filesystem::is_directory(path)) path.append("/index.html"); boost::beast::error_code ec; http::file_body::value_type body; body.open(path.c_str(), boost::beast::file_mode::scan, ec); if (ec == boost::beast::errc::no_such_file_or_directory) { std::ostringstream oss; oss << "The resource '" << target << "' was not found."; LOG(error) << oss.str(); session.errorReply(request, http::status::not_found, oss.str()); return; } else if (ec) { session.reply(ServiceLogic::serverError(request, ec.message())); return; } auto const size = body.size(); http::response res{std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(http::status::ok, request.version())}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, ResponseUtility::mimeType(path)); res.content_length(size); res.keep_alive(request.keep_alive()); session.reply(std::move(res)); }); // clang-format on } void visitAnalysis() { using namespace Core; // clang-format off static std::vector urlFilter = { "/", "/search", "/login", "/redirect", "/MessageBoard", "/我的笔记", "/我的笔记/", "/我的博客", "/我的博客/2020/11/22/如何给岛国小电影找字幕", }; // clang-format on auto application = Singleton::instance(); application->insertUrl("/api/v1/visit_analysis", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { using namespace std::chrono; using namespace boost::beast; using namespace Core; auto rootJson = boost::json::parse(request.body()); auto &root = rootJson.as_object(); std::string url; if (root.contains("url")) { url = root["url"].as_string(); if (!url.empty() && (url.back() == '/')) { url.pop_back(); } } auto database = Singleton::instance(); auto settings = Singleton::instance(); auto documentRoot = settings->documentRoot(); if (!url.empty() && (documentRoot.empty() || std::filesystem::exists(documentRoot + url))) { if (url.find("/我的博客/page") != 0) { // Docusaurus 这个路径过滤掉 if (root.contains("visitor_uuid") && root.contains("user_agent")) { auto timestamp = duration_cast(system_clock::now().time_since_epoch()); auto visitorUuid = std::string(root["visitor_uuid"].as_string()); auto userAgent = std::string(root["user_agent"].as_string()); database->upsertVisitRecord(url, visitorUuid, userAgent, timestamp.count()); } } } auto urlStats = database->visitorStats(url); auto siteStats = database->siteStats(); boost::json::object reply; reply["page_view_count"] = urlStats.totalViews; reply["unique_visitor_count"] = urlStats.visitorCount; reply["site_page_view_count"] = siteStats.totalViews; reply["site_unique_visitor_count"] = siteStats.totalVisitors; http::response s{boost::beast::http::status::ok, request.version()}; s.set(http::field::server, BOOST_BEAST_VERSION_STRING); s.set(http::field::content_type, "application/json;charset=UTF-8"); s.keep_alive(request.keep_alive()); s.body() = boost::json::serialize(reply); s.prepare_payload(); session.reply(std::move(s)); }); application->insertUrl("/api/v1/most_viewed_urls", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { using namespace boost::beast; int size = 5; std::error_code error; if (!request.body().empty()) { auto rootJson = boost::json::parse(request.body(), error); if (error) { LOG(info) << "<" << request.body() << "> parse json error: " << error.message(); } else { auto &root = rootJson.as_object(); if (root.contains("size")) { size = root.at("size").as_int64(); } } } auto database = Singleton::instance(); auto stats = database->mostViewedUrls(size + urlFilter.size()); boost::json::array reply; int index = 0; for (auto stat : stats) { if (std::find(urlFilter.cbegin(), urlFilter.cend(), stat.url) != urlFilter.cend()) continue; boost::json::object object; object["url"] = stat.url; object["count"] = stat.totalViews; reply.push_back(std::move(object)); index++; if (index >= size) break; } http::response s{boost::beast::http::status::ok, request.version()}; s.set(http::field::server, BOOST_BEAST_VERSION_STRING); s.set(http::field::content_type, "application/json;charset=UTF-8"); s.keep_alive(request.keep_alive()); s.body() = boost::json::serialize(reply); s.prepare_payload(); session.reply(std::move(s)); }); application->insertUrl( "/api/v1/latest_viewed_urls", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { using namespace boost::beast; using namespace std::chrono; int size = 5; std::error_code error; if (!request.body().empty()) { auto rootJson = boost::json::parse(request.body(), error); if (error) { LOG(info) << "<" << request.body() << "> parse json error: " << error.message(); } else { auto &root = rootJson.as_object(); if (root.contains("size")) { size = root.at("size").as_int64(); } } } auto database = Singleton::instance(); auto stats = database->latestViewedUrls(size + urlFilter.size()); boost::json::array reply; int index = 0; for (auto &stat : stats) { if (std::find(urlFilter.cbegin(), urlFilter.cend(), stat.url) != urlFilter.cend()) continue; boost::json::object object; object["url"] = stat.url; object["time"] = stat.lastViewTime; reply.push_back(std::move(object)); index++; if (index >= size) break; } http::response s{boost::beast::http::status::ok, request.version()}; s.set(http::field::server, BOOST_BEAST_VERSION_STRING); s.set(http::field::content_type, "application/json;charset=UTF-8"); s.keep_alive(request.keep_alive()); s.body() = boost::json::serialize(reply); s.prepare_payload(); session.reply(std::move(s)); }); } void live2dBackend() { using namespace Core; auto application = Singleton::instance(); application->insertUrl("/api/v1/live2d/{path*}", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { auto settings = Singleton::instance(); using namespace boost::beast; boost::urls::url_view view(request.target()); auto target = view.path(); // LOG(info) << target; if (target.find("..") != boost::beast::string_view::npos) { session.reply(ServiceLogic::badRequest(request, "Illegal request-target")); return; } std::string path = ResponseUtility::pathCat(settings->live2dModelsRoot(), matches["path"]); if (target.back() == '/') path.append("index.html"); if (std::filesystem::is_directory(path)) path.append("/index.html"); boost::beast::error_code ec; http::file_body::value_type body; body.open(path.c_str(), boost::beast::file_mode::scan, ec); if (ec == boost::beast::errc::no_such_file_or_directory) { std::ostringstream oss; oss << "The resource '" << target << "' was not found."; LOG(error) << oss.str(); session.errorReply(request, http::status::not_found, oss.str()); return; } else if (ec) { session.reply(ServiceLogic::serverError(request, ec.message())); return; } auto const size = body.size(); http::response res{std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(http::status::ok, request.version())}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, ResponseUtility::mimeType(path)); // res.set(http::field::access_control_allow_origin, "*"); res.set(http::field::cache_control, "max-age=2592000"); res.set(http::field::expires, "Fri, 22 Nov 2124 13:30:28 GMT"); res.content_length(size); res.keep_alive(request.keep_alive()); session.reply(std::move(res)); }); } void userAccount() { using namespace Core; auto application = Singleton::instance(); application->insertUrl("/api/v1/user/register", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { auto rootJson = boost::json::parse(request.body()); auto &root = rootJson.as_object(); boost::json::object reply; if (root.contains("username") && root.contains("password") && root.contains("email")) { try { auto account = Older::Account::hashPassword(std::string(root.at("password").as_string())); account.username = root.at("username").as_string(); account.email = root.at("email").as_string(); auto database = Singleton::instance(); database->createUser(account); reply["code"] = 200; reply["message"] = "register success"; account = database->user(account.username); boost::json::object data; data["user_id"] = account.id; reply["data"] = std::move(data); } catch (const std::exception &e) { reply["code"] = 400; reply["message"] = e.what(); } } else { reply["code"] = 400; reply["message"] = "missing username, password or email"; } http::response s{boost::beast::http::status::ok, request.version()}; s.set(http::field::server, BOOST_BEAST_VERSION_STRING); s.set(http::field::content_type, "application/json;charset=UTF-8"); s.keep_alive(request.keep_alive()); s.body() = boost::json::serialize(reply); s.prepare_payload(); session.reply(std::move(s)); }); application->insertUrl("/api/v1/user/login", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { auto rootJson = boost::json::parse(request.body()); auto &root = rootJson.as_object(); boost::json::object reply; std::string cookie; if (root.contains("username") && root.contains("password")) { try { auto database = Singleton::instance(); auto sessions = Singleton::instance(); auto account = database->user(std::string(root.at("username").as_string())); bool logined = Older::Account::verifyPassword(account, std::string(root.at("password").as_string())); reply["code"] = logined ? 200 : 404; reply["message"] = logined ? "login success" : "wrong password or user"; account = database->user(account.username); if (logined) { boost::json::object data; data["user_id"] = account.id; reply["data"] = std::move(data); // clang-format off cookie = "older_auth=" + sessions->addSession(account.id) + "; Path=/" + "; HttpOnly" // 阻止JS访问 + "; Secure" // 仅HTTPS传输 + "; SameSite=Strict" // 防止CSRF + "; Max-Age=86400"; // 24小时过期 // clang-format on } } catch (const std::exception &e) { reply["code"] = 400; reply["message"] = e.what(); } } else { reply["code"] = 400; reply["message"] = "missing username, password or email"; } http::response s{boost::beast::http::status::ok, request.version()}; s.set(http::field::server, BOOST_BEAST_VERSION_STRING); s.set(http::field::content_type, "application/json;charset=UTF-8"); if (!cookie.empty()) { s.set(http::field::set_cookie, cookie); } s.keep_alive(request.keep_alive()); s.body() = boost::json::serialize(reply); s.prepare_payload(); session.reply(std::move(s)); }); application->insertUrl("/api/v1/user/verify", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { auto cookies = request.find(http::field::cookie); boost::json::object reply; std::string cookie; if (cookies == request.end()) { reply["code"] = 401; reply["message"] = "cookie is not exists"; } else { auto sessions = Singleton::instance(); auto database = Singleton::instance(); std::string accessToken = extractToken(cookies->value(), "older_auth"); auto [valid, newAccessToken] = sessions->validateAndRefresh(accessToken); if (valid) { if (newAccessToken != accessToken) { // 需要刷新令牌 // clang-format off cookie = "older_auth=" + newAccessToken + "; Path=/" + "; HttpOnly" // 阻止JS访问 + "; Secure" // 仅HTTPS传输 + "; SameSite=Strict" // 防止CSRF + "; Max-Age=86400"; // 24小时过期 // clang-format on } reply["code"] = 200; reply["message"] = "verify success"; boost::json::object data; auto d = sessions->at(newAccessToken); auto account = database->user(d.userId); data["user_id"] = d.userId; data["username"] = account.username; data["email"] = account.email; reply["data"] = std::move(data); } else { reply["code"] = 401; reply["message"] = "cookie is invalid"; } } http::response s{boost::beast::http::status::ok, request.version()}; s.set(http::field::server, BOOST_BEAST_VERSION_STRING); s.set(http::field::content_type, "application/json;charset=UTF-8"); if (!cookie.empty()) { s.set(http::field::set_cookie, cookie); } s.keep_alive(request.keep_alive()); s.body() = boost::json::serialize(reply); s.prepare_payload(); session.reply(std::move(s)); }); application->insertUrl("/api/v1/user/logout", [](Older::HttpSession &session, const Older::Application::Request &request, const boost::urls::matches &matches) { http::response res{boost::beast::http::status::ok, request.version()}; try { auto sessions = Singleton::instance(); auto cookies = request.find(http::field::cookie); if (cookies != request.end()) { std::string accessToken = extractToken(cookies->value(), "older_auth"); if (!accessToken.empty()) { sessions->removeSession(accessToken); } } res.set(http::field::content_type, "application/json"); res.set(http::field::set_cookie, "older_auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure"); res.set(http::field::set_cookie, "older_refresh_token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure"); res.body() = R"({"code": 401, "message": "logout successfully"})"; } catch (const std::exception &e) { res.result(http::status::internal_server_error); res.body() = R"({"status": "error", "message": ")" + std::string(e.what()) + "\"}"; } session.reply(std::move(res)); }); } } // namespace ServiceLogic