Game Features
This commit is contained in:
+100
-9
@@ -1,5 +1,9 @@
|
||||
#include "server.hpp"
|
||||
#include "store.hpp"
|
||||
#include "application.hpp"
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
@@ -10,6 +14,7 @@
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
@@ -122,30 +127,116 @@ auto fpol::run(fpol::server_options const &options) -> int {
|
||||
io.run();
|
||||
initialized.get();
|
||||
io.restart();
|
||||
for (auto const &action : {"rules", "catalog", "capacity", "score-preview"}) {
|
||||
auto method = std::string_view{action} == "rules" || std::string_view{action} == "catalog" ? "GET" : "POST";
|
||||
router.add_route(method, std::string{"/api/"} + action, [&store, action = std::string{action}](ligo::request_context, boost::json::value input) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
|
||||
try {
|
||||
co_return co_await store.handle(action, std::move(input));
|
||||
} catch (std::invalid_argument const &error) {
|
||||
co_return std::unexpected(ligo::detail::make_error(ligo::detail::error_invalid_argument, error.what()));
|
||||
} catch (std::out_of_range const &error) {
|
||||
co_return std::unexpected(ligo::detail::make_error(ligo::detail::error_invalid_argument, error.what()));
|
||||
auto application = fpol::application{{options.database, options.allow_demo, {}}};
|
||||
auto restarted = boost::asio::co_spawn(io, application.tick(true), boost::asio::use_future);
|
||||
io.run();
|
||||
restarted.get();
|
||||
io.restart();
|
||||
auto reads = std::set<std::string>{"auth/me", "leagues", "league", "catalogs", "candidates", "results", "standings", "admin/sources"};
|
||||
auto actions = reads;
|
||||
for (auto const &action : {"auth/login", "auth/register", "auth/logout", "auth/password", "auth/reset", "league/create", "league/join", "league/invite", "league/revoke-invites", "league/remove", "league/settings", "league/name", "league/ratings", "team/name", "draft/queue", "draft/pick", "draft/start", "draft/pause", "draft/resume"}) {
|
||||
actions.insert(action);
|
||||
}
|
||||
for (auto const &action : actions) {
|
||||
auto method = reads.contains(action) ? "GET" : "POST";
|
||||
router.add_response_route(method, "/api/" + action, [&application, &options, action, read = reads.contains(action)](ligo::request_context context, boost::json::value input) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
auto session = std::string{};
|
||||
auto cookie = context.headers["cookie"];
|
||||
for (std::size_t start = 0; start < cookie.size();) {
|
||||
auto end = cookie.find(';', start);
|
||||
if (end == std::string::npos) {
|
||||
end = cookie.size();
|
||||
}
|
||||
auto part = cookie.substr(start, end - start);
|
||||
auto first = part.find_first_not_of(' ');
|
||||
if (first != std::string::npos) {
|
||||
part.erase(0, first);
|
||||
}
|
||||
if (part.starts_with("fpol_session=")) {
|
||||
if (!session.empty()) {
|
||||
session.clear();
|
||||
break;
|
||||
}
|
||||
session = part.substr(13);
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
auto result = boost::json::value{};
|
||||
if (!read && (context.headers["origin"] != options.origin || context.headers["x-fpol-request"] != "1")) {
|
||||
result = boost::json::object{{"ok", false}, {"status", 403}, {"message", "Request origin is not allowed"}};
|
||||
} else {
|
||||
if (read) {
|
||||
input = boost::json::object{};
|
||||
for (auto const &[key, value] : context.query) {
|
||||
input.as_object()[key] = value;
|
||||
}
|
||||
}
|
||||
result = co_await application.handle(action, std::move(input), session, context.headers["x-csrf-token"], context.peer);
|
||||
}
|
||||
auto response = ligo::service_response{};
|
||||
response.status = static_cast<std::uint16_t>(result.at("status").as_int64());
|
||||
response.headers = {{"Cache-Control", "no-store"}, {"X-Content-Type-Options", "nosniff"}};
|
||||
auto suffix = std::string{"; Path=/; HttpOnly; SameSite=Lax"} + (options.secure_cookies ? "; Secure" : "");
|
||||
if (auto token = result.as_object().if_contains("session_token")) {
|
||||
response.headers.emplace_back("Set-Cookie", "fpol_session=" + std::string{token->as_string()} + "; Max-Age=2592000" + suffix);
|
||||
result.as_object().erase("session_token");
|
||||
}
|
||||
if (result.as_object().contains("clear_session")) {
|
||||
response.headers.emplace_back("Set-Cookie", "fpol_session=; Max-Age=0" + suffix);
|
||||
result.as_object().erase("clear_session");
|
||||
}
|
||||
response.body = boost::json::serialize(result);
|
||||
co_return response;
|
||||
});
|
||||
}
|
||||
router.add_route("GET", "/api/rules", [&store](ligo::request_context, boost::json::value input) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> { co_return co_await store.handle("rules", std::move(input)); });
|
||||
// The snapshot simulator stays a local development tool, explicitly enabled.
|
||||
if (options.allow_demo) {
|
||||
for (auto const &action : {"catalog", "capacity", "score-preview"}) {
|
||||
auto method = std::string_view{action} == "catalog" ? "GET" : "POST";
|
||||
router.add_route(method, std::string{"/api/"} + action, [&store, action = std::string{action}](ligo::request_context, boost::json::value input) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
|
||||
try {
|
||||
co_return co_await store.handle(action, std::move(input));
|
||||
} catch (std::exception const &error) {
|
||||
co_return std::unexpected(ligo::detail::make_error(ligo::detail::error_invalid_argument, error.what()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
auto http_options = ligo::http::server_options{};
|
||||
http_options.parse_query = true;
|
||||
http_options.max_body_size = 64 * 1024;
|
||||
http_options.on_error = [](modulus::error error) { modulus::log_warn(error.describe()); };
|
||||
auto server = ligo::http::server{io, {.host = options.host, .port = options.port}, std::move(router), std::move(http_options)};
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
auto timer = boost::asio::steady_timer{io};
|
||||
auto stopping = false;
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
modulus::log_error(started.error().describe());
|
||||
return 1;
|
||||
}
|
||||
auto exit_code = 0;
|
||||
auto ticking = [&]() -> boost::asio::awaitable<void> {
|
||||
while (!stopping) {
|
||||
timer.expires_after(std::chrono::seconds{1});
|
||||
auto error = boost::system::error_code{};
|
||||
co_await timer.async_wait(boost::asio::redirect_error(boost::asio::use_awaitable, error));
|
||||
if (error || stopping) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
co_await application.tick();
|
||||
} catch (std::exception const &failure) {
|
||||
modulus::log_error(failure.what());
|
||||
}
|
||||
}
|
||||
};
|
||||
boost::asio::co_spawn(io, ticking(), [&](std::exception_ptr failure) {if(failure){exit_code=1;io.stop();} });
|
||||
signals.async_wait([&](boost::system::error_code error, int) {
|
||||
if (!error) {
|
||||
stopping = true;
|
||||
timer.cancel();
|
||||
modulus::log_info("Stopping server");
|
||||
boost::asio::co_spawn(io, stop_server(server), [&](std::exception_ptr failure) {
|
||||
if (failure) {
|
||||
|
||||
Reference in New Issue
Block a user