From 050a475304435a216b445b197a6f2f2ac7f9731b Mon Sep 17 00:00:00 2001 From: jie Date: Mon, 18 Oct 2021 13:59:21 +0800 Subject: [PATCH] project has been restructured to adapt to windows --- .gitignore | 9 +- .gitmodules | 12 + CMakeLists.txt | 19 - LICENSE | 2 +- README.md | 61 +-- WebApp/WebApp.sln | 44 ++ WebApp/WebApp/WebApp.cpp | 137 +++++ WebApp/WebApp/WebApp.vcxproj | 168 ++++++ WebApp/WebApp/WebApp.vcxproj.filters | 36 ++ WebApp/WebApp/WebApp.vcxproj.user | 14 + WebApp/WebApp/handlers.cpp | 390 +++++++++++++ WebApp/WebApp/handlers.h | 76 +++ WebApp/WebApp/rendering.cpp | 84 +++ WebApp/WebApp/rendering.h | 24 + WebApp/bserv/bserv.cpp | 511 ++++++++++++++++++ WebApp/bserv/bserv.vcxproj | 190 +++++++ WebApp/bserv/bserv.vcxproj.filters | 75 +++ WebApp/bserv/bserv.vcxproj.user | 4 + WebApp/bserv/client.cpp | 75 +++ WebApp/bserv/database.cpp | 36 ++ WebApp/bserv/framework.h | 3 + WebApp/bserv/include/bserv/client.hpp | 145 +++++ .../bserv/include/bserv}/common.hpp | 2 + WebApp/bserv/include/bserv/config.hpp | 50 ++ WebApp/bserv/include/bserv/database.hpp | 355 ++++++++++++ WebApp/bserv/include/bserv/logging.hpp | 57 ++ WebApp/bserv/include/bserv/router.hpp | 431 +++++++++++++++ .../bserv/include/bserv}/server.hpp | 40 +- WebApp/bserv/include/bserv/session.hpp | 69 +++ WebApp/bserv/include/bserv/utils.hpp | 62 +++ WebApp/bserv/include/bserv/websocket.hpp | 65 +++ WebApp/bserv/pch.cpp | 5 + WebApp/bserv/pch.h | 15 + WebApp/bserv/session.cpp | 50 ++ WebApp/bserv/utils.cpp | 236 ++++++++ bserv/CMakeLists.txt | 26 - bserv/client.hpp | 204 ------- bserv/config.hpp | 50 -- bserv/database.hpp | 380 ------------- bserv/logging.hpp | 48 -- bserv/router.hpp | 414 -------------- bserv/server.cpp | 503 ----------------- bserv/session.hpp | 108 ---- bserv/utils.hpp | 246 --------- bserv/websocket.hpp | 65 --- config.json | 8 + dependencies/README.md | 58 ++ dependencies/boost | 1 + dependencies/cryptopp | 1 + dependencies/inja | 1 + dependencies/libpqxx | 1 + handlers.hpp | 267 --------- main.cpp | 137 ----- templates/base.html | 101 ++++ templates/index.html | 32 ++ templates/statics/css/bootstrap.min.css | 7 + templates/statics/css/bootstrap.min.css.map | 1 + templates/statics/js/bootstrap.bundle.min.js | 7 + .../statics/js/bootstrap.bundle.min.js.map | 1 + templates/users.html | 121 +++++ {scripts => test/scripts}/db_test.py | 32 +- {scripts => test/scripts}/request_test.py | 32 +- {scripts => test/scripts}/ws_test.py | 23 +- 63 files changed, 3835 insertions(+), 2592 deletions(-) create mode 100644 .gitmodules delete mode 100644 CMakeLists.txt create mode 100644 WebApp/WebApp.sln create mode 100644 WebApp/WebApp/WebApp.cpp create mode 100644 WebApp/WebApp/WebApp.vcxproj create mode 100644 WebApp/WebApp/WebApp.vcxproj.filters create mode 100644 WebApp/WebApp/WebApp.vcxproj.user create mode 100644 WebApp/WebApp/handlers.cpp create mode 100644 WebApp/WebApp/handlers.h create mode 100644 WebApp/WebApp/rendering.cpp create mode 100644 WebApp/WebApp/rendering.h create mode 100644 WebApp/bserv/bserv.cpp create mode 100644 WebApp/bserv/bserv.vcxproj create mode 100644 WebApp/bserv/bserv.vcxproj.filters create mode 100644 WebApp/bserv/bserv.vcxproj.user create mode 100644 WebApp/bserv/client.cpp create mode 100644 WebApp/bserv/database.cpp create mode 100644 WebApp/bserv/framework.h create mode 100644 WebApp/bserv/include/bserv/client.hpp rename {bserv => WebApp/bserv/include/bserv}/common.hpp (82%) create mode 100644 WebApp/bserv/include/bserv/config.hpp create mode 100644 WebApp/bserv/include/bserv/database.hpp create mode 100644 WebApp/bserv/include/bserv/logging.hpp create mode 100644 WebApp/bserv/include/bserv/router.hpp rename {bserv => WebApp/bserv/include/bserv}/server.hpp (59%) create mode 100644 WebApp/bserv/include/bserv/session.hpp create mode 100644 WebApp/bserv/include/bserv/utils.hpp create mode 100644 WebApp/bserv/include/bserv/websocket.hpp create mode 100644 WebApp/bserv/pch.cpp create mode 100644 WebApp/bserv/pch.h create mode 100644 WebApp/bserv/session.cpp create mode 100644 WebApp/bserv/utils.cpp delete mode 100644 bserv/CMakeLists.txt delete mode 100644 bserv/client.hpp delete mode 100644 bserv/config.hpp delete mode 100644 bserv/database.hpp delete mode 100644 bserv/logging.hpp delete mode 100644 bserv/router.hpp delete mode 100644 bserv/server.cpp delete mode 100644 bserv/session.hpp delete mode 100644 bserv/utils.hpp delete mode 100644 bserv/websocket.hpp create mode 100644 config.json create mode 100644 dependencies/README.md create mode 160000 dependencies/boost create mode 160000 dependencies/cryptopp create mode 160000 dependencies/inja create mode 160000 dependencies/libpqxx delete mode 100644 handlers.hpp delete mode 100644 main.cpp create mode 100644 templates/base.html create mode 100644 templates/index.html create mode 100644 templates/statics/css/bootstrap.min.css create mode 100644 templates/statics/css/bootstrap.min.css.map create mode 100644 templates/statics/js/bootstrap.bundle.min.js create mode 100644 templates/statics/js/bootstrap.bundle.min.js.map create mode 100644 templates/users.html rename {scripts => test/scripts}/db_test.py (79%) rename {scripts => test/scripts}/request_test.py (69%) rename {scripts => test/scripts}/ws_test.py (82%) diff --git a/.gitignore b/.gitignore index 324b3b0..69ce43a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -test/ -build/* -!build/README.md -.* +.vscode +.vs +x64 +log +pgsql # Prerequisites *.d diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3eeb42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "dependencies\\boost"] + path = dependencies\\boost + url = https://github.com/boostorg/boost +[submodule "dependencies/cryptopp"] + path = dependencies/cryptopp + url = https://github.com/weidai11/cryptopp +[submodule "dependencies/libpqxx"] + path = dependencies/libpqxx + url = https://github.com/jtv/libpqxx +[submodule "dependencies/inja"] + path = dependencies/inja + url = https://github.com/pantor/inja diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index c849fb3..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 3.10) - -project(bserv_main) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED True) - -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -set(CMAKE_CXX_FLAGS "-Wall -Wextra") -set(CMAKE_CXX_FLAGS_DEBUG "-g") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") - -add_subdirectory(bserv) - -add_executable(main main.cpp) -target_link_libraries(main bserv) diff --git a/LICENSE b/LICENSE index b4b2e34..6e4ee9d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Jonathan +Copyright (c) 2021 Jie Shi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1ac0b1a..c873925 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,6 @@ # bserv -*A Boost Based High Performance C++ HTTP JSON Server.* - - -## Dependencies - -- [Boost 1.75.0](https://www.boost.org/) -- [PostgreSQL 13.2](https://www.postgresql.org/) -- [Libpqxx 7.3.1](https://github.com/jtv/libpqxx) -- [Crypto++ 8.4.0](https://cryptopp.com/) -- CMake +*A Boost Based C++ HTTP JSON Server.* ## Quick Start @@ -29,55 +20,7 @@ You can import the sample database: ``` -### Server & Routing - -Configure server & routing in [main.cpp](main.cpp). - - -### Handlers - -Write the handlers in [handlers.hpp](handlers.hpp) - - ## Build -In the `shell`: +Refer to [readme](/dependencies/README.md) for setting up dependencies. -- Create a directory `build`, and enter it: - ``` - mkdir build - cd build - ``` -- Run: - ``` - cmake .. - ``` -- Build: - ``` - cmake --build . - ``` - - -## Running - -In `build`, run in `shell`: -``` -./bserv -``` - - -## Performance - -This test is performed by Jmeter. The unit for throughput is Transaction per second. - -|URL|bserv| -|:-:|:-:| -|`/login`|139.55| -|`/find/`|958.77| - -For `/login`, we intentionally slow down the attacker's speed. - - -### Computer Hardware: -- Intel Core i9-9900K x 4 -- 16GB RAM diff --git a/WebApp/WebApp.sln b/WebApp/WebApp.sln new file mode 100644 index 0000000..a9bfba3 --- /dev/null +++ b/WebApp/WebApp.sln @@ -0,0 +1,44 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31727.386 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebApp", "WebApp\WebApp.vcxproj", "{D46CB9A5-375A-470B-B9B0-1353862A18F5}" + ProjectSection(ProjectDependencies) = postProject + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296} = {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "bserv", "bserv\bserv.vcxproj", "{F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Debug|x64.ActiveCfg = Debug|x64 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Debug|x64.Build.0 = Debug|x64 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Debug|x86.ActiveCfg = Debug|Win32 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Debug|x86.Build.0 = Debug|Win32 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Release|x64.ActiveCfg = Release|x64 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Release|x64.Build.0 = Release|x64 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Release|x86.ActiveCfg = Release|Win32 + {D46CB9A5-375A-470B-B9B0-1353862A18F5}.Release|x86.Build.0 = Release|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x64.ActiveCfg = Debug|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x64.Build.0 = Debug|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x86.ActiveCfg = Debug|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Debug|x86.Build.0 = Debug|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x64.ActiveCfg = Release|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x64.Build.0 = Release|x64 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x86.ActiveCfg = Release|Win32 + {F5C0CF6D-7BF9-40A5-AF2E-8FC36A1D7296}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1F364622-40BA-47B1-BC0A-B226F6FC8CE8} + EndGlobalSection +EndGlobal diff --git a/WebApp/WebApp/WebApp.cpp b/WebApp/WebApp/WebApp.cpp new file mode 100644 index 0000000..3c446c1 --- /dev/null +++ b/WebApp/WebApp/WebApp.cpp @@ -0,0 +1,137 @@ +#include +#include +#include + +#include +#include "bserv/common.hpp" + +#include "rendering.h" +#include "handlers.h" + +void show_usage(const bserv::server_config& config) { + std::cout << "Usage: " << config.get_name() << " [config.json]\n" + << config.get_name() << " is a C++ HTTP server.\n\n" + "Example:\n" + << " " << config.get_name() << " config.json\n\n" + << std::endl; +} + +void show_config(const bserv::server_config& config) { + std::cout << config.get_name() << " config:" + << "\nport: " << config.get_port() + << "\nthreads: " << config.get_num_threads() + << "\nrotation: " << config.get_log_rotation_size() / 1024 / 1024 + << "\nlog path: " << config.get_log_path() + << "\ndb-conn: " << config.get_num_db_conn() + << "\nconn-str: " << config.get_db_conn_str() << std::endl; +} + +int main(int argc, char* argv[]) { + bserv::server_config config; + + if (argc != 2) { + show_usage(config); + return EXIT_FAILURE; + } + if (argc == 2) { + try { + std::string config_content = read_bin(argv[1]); + //std::cout << config_content << std::endl; + boost::json::object config_obj = boost::json::parse(config_content).as_object(); + if (config_obj.contains("port")) + config.set_port((unsigned short)config_obj["port"].as_int64()); + if (config_obj.contains("thread-num")) + config.set_num_threads((int)config_obj["thread-num"].as_int64()); + if (config_obj.contains("conn-num")) + config.set_num_db_conn((int)config_obj["conn-num"].as_int64()); + if (config_obj.contains("conn-str")) + config.set_db_conn_str(config_obj["conn-str"].as_string().c_str()); + if (!config_obj.contains("template_root")) { + std::cerr << "`template_root` must be specified" << std::endl; + return EXIT_FAILURE; + } + else init_rendering(config_obj["template_root"].as_string().c_str()); + if (!config_obj.contains("static_root")) { + std::cerr << "`static_root` must be specified" << std::endl; + return EXIT_FAILURE; + } + else init_static_root(config_obj["static_root"].as_string().c_str()); + } + catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + } + show_config(config); + + auto _ = bserv::server{ config, { + // rest api example + bserv::make_path("/hello", &hello, + bserv::placeholders::response, + bserv::placeholders::session), + bserv::make_path("/register", &user_register, + bserv::placeholders::request, + bserv::placeholders::json_params, + bserv::placeholders::db_connection_ptr), + bserv::make_path("/login", &user_login, + bserv::placeholders::request, + bserv::placeholders::json_params, + bserv::placeholders::db_connection_ptr, + bserv::placeholders::session), + bserv::make_path("/logout", &user_logout, + bserv::placeholders::session), + bserv::make_path("/find/", &find_user, + bserv::placeholders::db_connection_ptr, + bserv::placeholders::_1), + bserv::make_path("/send", &send_request, + bserv::placeholders::session, + bserv::placeholders::http_client_ptr, + bserv::placeholders::json_params), + bserv::make_path("/echo", &echo, + bserv::placeholders::json_params), + + // serving static files + bserv::make_path("/statics/", &serve_static_files, + bserv::placeholders::response, + bserv::placeholders::_1), + + // serving html template files + bserv::make_path("/", &index, + bserv::placeholders::session, + bserv::placeholders::response), + bserv::make_path("/form_login", &form_login, + bserv::placeholders::request, + bserv::placeholders::response, + bserv::placeholders::json_params, + bserv::placeholders::db_connection_ptr, + bserv::placeholders::session), + bserv::make_path("/form_logout", &form_logout, + bserv::placeholders::session, + bserv::placeholders::response), + bserv::make_path("/users", &view_users, + bserv::placeholders::db_connection_ptr, + bserv::placeholders::session, + bserv::placeholders::response, + std::string{"1"}), + bserv::make_path("/users/", &view_users, + bserv::placeholders::db_connection_ptr, + bserv::placeholders::session, + bserv::placeholders::response, + bserv::placeholders::_1), + bserv::make_path("/form_add_user", &form_add_user, + bserv::placeholders::request, + bserv::placeholders::response, + bserv::placeholders::json_params, + bserv::placeholders::db_connection_ptr, + bserv::placeholders::session), + } + , { + // websocket example + bserv::make_path("/echo", &ws_echo, + bserv::placeholders::session, + bserv::placeholders::websocket_server_ptr) + } + }; + + return EXIT_SUCCESS; +} diff --git a/WebApp/WebApp/WebApp.vcxproj b/WebApp/WebApp/WebApp.vcxproj new file mode 100644 index 0000000..0e9933b --- /dev/null +++ b/WebApp/WebApp/WebApp.vcxproj @@ -0,0 +1,168 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {d46cb9a5-375a-470b-b9b0-1353862a18f5} + WebApp + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + ..\..\dependencies\inja\include;..\..\dependencies\inja\third_party\include;..\bserv\include;..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;%(AdditionalIncludeDirectories) + /bigobj %(AdditionalOptions) + + + Console + true + $(OutDir);..\..\dependencies\libpqxx\src\Debug;..\..\dependencies\pgsql\lib;..\..\dependencies\cryptopp\x64\Output\Debug;..\..\dependencies\boost\stage\lib;%(AdditionalLibraryDirectories) + bserv.lib;cryptlib.lib;pqxx.lib;libpq.lib;wsock32.lib;ws2_32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;%(AdditionalDependencies) + + + xcopy /y /d "..\..\dependencies\pgsql\bin\*.dll" "$(OutDir)" + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + ..\..\dependencies\inja\include;..\..\dependencies\inja\third_party\include;..\bserv\include;..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;%(AdditionalIncludeDirectories) + + + Console + true + true + true + $(OutDir);..\..\dependencies\libpqxx\src\Release;..\..\dependencies\pgsql\lib;..\..\dependencies\cryptopp\x64\Output\Release;..\..\dependencies\boost\stage\lib;%(AdditionalLibraryDirectories) + bserv.lib;cryptlib.lib;pqxx.lib;libpq.lib;wsock32.lib;ws2_32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;%(AdditionalDependencies) + + + xcopy /y /d "..\..\dependencies\pgsql\bin\*.dll" "$(OutDir)" + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WebApp/WebApp/WebApp.vcxproj.filters b/WebApp/WebApp/WebApp.vcxproj.filters new file mode 100644 index 0000000..51196e5 --- /dev/null +++ b/WebApp/WebApp/WebApp.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 源文件 + + + 源文件 + + + 源文件 + + + + + 头文件 + + + 头文件 + + + \ No newline at end of file diff --git a/WebApp/WebApp/WebApp.vcxproj.user b/WebApp/WebApp/WebApp.vcxproj.user new file mode 100644 index 0000000..8bd0ba4 --- /dev/null +++ b/WebApp/WebApp/WebApp.vcxproj.user @@ -0,0 +1,14 @@ + + + + WindowsLocalDebugger + + + ..\..\config.json + WindowsLocalDebugger + + + ..\..\config.json + WindowsLocalDebugger + + \ No newline at end of file diff --git a/WebApp/WebApp/handlers.cpp b/WebApp/WebApp/handlers.cpp new file mode 100644 index 0000000..51350e7 --- /dev/null +++ b/WebApp/WebApp/handlers.cpp @@ -0,0 +1,390 @@ +#include "handlers.h" + +#include + +#include "rendering.h" + +// register an orm mapping (to convert the db query results into +// json objects). +// the db query results contain several rows, each has a number of +// fields. the order of `make_db_field(name[i])` in the +// initializer list corresponds to these fields (`Type[0]` and +// `name[0]` correspond to field[0], `Type[1]` and `name[1]` +// correspond to field[1], ...). `Type[i]` is the type you want +// to convert the field value to, and `name[i]` is the identifier +// with which you want to store the field in the json object, so +// if the returned json object is `obj`, `obj[name[i]]` will have +// the type of `Type[i]` and store the value of field[i]. +bserv::db_relation_to_object orm_user{ + bserv::make_db_field("id"), + bserv::make_db_field("username"), + bserv::make_db_field("password"), + bserv::make_db_field("is_superuser"), + bserv::make_db_field("first_name"), + bserv::make_db_field("last_name"), + bserv::make_db_field("email"), + bserv::make_db_field("is_active") +}; + +std::optional get_user( + bserv::db_transaction& tx, + const std::string& username) { + bserv::db_result r = tx.exec( + "select * from auth_user where username = ?", username); + lginfo << r.query(); // this is how you log info + return orm_user.convert_to_optional(r); +} + +std::string get_or_empty( + boost::json::object& obj, + const std::string& key) { + return obj.count(key) ? obj[key].as_string().c_str() : ""; +} + +// if you want to manually modify the response, +// the return type should be `std::nullopt_t`, +// and the return value should be `std::nullopt`. +std::nullopt_t hello( + bserv::response_type& response, + std::shared_ptr session_ptr) { + bserv::session_type& session = *session_ptr; + boost::json::object obj; + if (session.count("user")) { + // NOTE: modifications to sessions must be performed + // BEFORE referencing objects in them. this is because + // modifications might invalidate referenced objects. + // in this example, "count" might be added to `session`, + // which should be performed first. + // then `user` can be referenced safely. + if (!session.count("count")) { + session["count"] = 0; + } + auto& user = session["user"].as_object(); + session["count"] = session["count"].as_int64() + 1; + obj = { + {"welcome", user["username"]}, + {"count", session["count"]} + }; + } + else { + obj = { {"msg", "hello, world!"} }; + } + // the response body is a string, + // so the `obj` should be serialized + response.body() = boost::json::serialize(obj); + response.prepare_payload(); // this line is important! + return std::nullopt; +} + +// if you return a json object, the serialization +// is performed automatically. +boost::json::object user_register( + bserv::request_type& request, + // the json object is obtained from the request body, + // as well as the url parameters + boost::json::object&& params, + std::shared_ptr conn) { + if (request.method() != boost::beast::http::verb::post) { + throw bserv::url_not_found_exception{}; + } + if (params.count("username") == 0) { + return { + {"success", false}, + {"message", "`username` is required"} + }; + } + if (params.count("password") == 0) { + return { + {"success", false}, + {"message", "`password` is required"} + }; + } + auto username = params["username"].as_string(); + bserv::db_transaction tx{ conn }; + auto opt_user = get_user(tx, username.c_str()); + if (opt_user.has_value()) { + return { + {"success", false}, + {"message", "`username` existed"} + }; + } + auto password = params["password"].as_string(); + bserv::db_result r = tx.exec( + "insert into ? " + "(?, password, is_superuser, " + "first_name, last_name, email, is_active) values " + "(?, ?, ?, ?, ?, ?, ?)", bserv::db_name("auth_user"), + bserv::db_name("username"), + username.c_str(), + bserv::utils::security::encode_password( + password.c_str()), false, + get_or_empty(params, "first_name"), + get_or_empty(params, "last_name"), + get_or_empty(params, "email"), true); + lginfo << r.query(); + tx.commit(); // you must manually commit changes + return { + {"success", true}, + {"message", "user registered"} + }; +} + +boost::json::object user_login( + bserv::request_type& request, + boost::json::object&& params, + std::shared_ptr conn, + std::shared_ptr session_ptr) { + if (request.method() != boost::beast::http::verb::post) { + throw bserv::url_not_found_exception{}; + } + if (params.count("username") == 0) { + return { + {"success", false}, + {"message", "`username` is required"} + }; + } + if (params.count("password") == 0) { + return { + {"success", false}, + {"message", "`password` is required"} + }; + } + auto username = params["username"].as_string(); + bserv::db_transaction tx{ conn }; + auto opt_user = get_user(tx, username.c_str()); + if (!opt_user.has_value()) { + return { + {"success", false}, + {"message", "invalid username/password"} + }; + } + auto& user = opt_user.value(); + if (!user["is_active"].as_bool()) { + return { + {"success", false}, + {"message", "invalid username/password"} + }; + } + auto password = params["password"].as_string(); + auto encoded_password = user["password"].as_string(); + if (!bserv::utils::security::check_password( + password.c_str(), encoded_password.c_str())) { + return { + {"success", false}, + {"message", "invalid username/password"} + }; + } + bserv::session_type& session = *session_ptr; + session["user"] = user; + return { + {"success", true}, + {"message", "login successfully"} + }; +} + +boost::json::object find_user( + std::shared_ptr conn, + const std::string& username) { + bserv::db_transaction tx{ conn }; + auto user = get_user(tx, username); + if (!user.has_value()) { + return { + {"success", false}, + {"message", "requested user does not exist"} + }; + } + user.value().erase("id"); + user.value().erase("password"); + return { + {"success", true}, + {"user", user.value()} + }; +} + +boost::json::object user_logout( + std::shared_ptr session_ptr) { + bserv::session_type& session = *session_ptr; + if (session.count("user")) { + session.erase("user"); + } + return { + {"success", true}, + {"message", "logout successfully"} + }; +} + +boost::json::object send_request( + std::shared_ptr session, + std::shared_ptr client_ptr, + boost::json::object&& params) { + // post for response: + // auto res = client_ptr->post( + // "localhost", "8080", "/echo", {{"msg", "request"}} + // ); + // return {{"response", boost::json::parse(res.body())}}; + // ------------------------------------------------------- + // - if it takes longer than 30 seconds (by default) to + // - get the response, this will raise a read timeout + // ------------------------------------------------------- + // post for json response (json value, rather than json + // object, is returned): + auto obj = client_ptr->post_for_value( + "localhost", "8080", "/echo", { {"request", params} } + ); + if (session->count("cnt") == 0) { + (*session)["cnt"] = 0; + } + (*session)["cnt"] = (*session)["cnt"].as_int64() + 1; + return { {"response", obj}, {"cnt", (*session)["cnt"]} }; +} + +boost::json::object echo( + boost::json::object&& params) { + return { {"echo", params} }; +} + +// websocket +std::nullopt_t ws_echo( + std::shared_ptr session, + std::shared_ptr ws_server) { + ws_server->write_json((*session)["cnt"]); + while (true) { + try { + std::string data = ws_server->read(); + ws_server->write(data); + } + catch (bserv::websocket_closed&) { + break; + } + } + return std::nullopt; +} + + +std::nullopt_t serve_static_files( + bserv::response_type& response, + const std::string& path) { + return serve(response, path); +} + + +std::nullopt_t index( + const std::string& template_path, + std::shared_ptr session_ptr, + bserv::response_type& response, + boost::json::object& context) { + bserv::session_type& session = *session_ptr; + if (session.contains("user")) { + context["user"] = session["user"]; + } + return render(response, template_path, context); +} + +std::nullopt_t index( + std::shared_ptr session_ptr, + bserv::response_type& response) { + boost::json::object context; + return index("index.html", session_ptr, response, context); +} + +std::nullopt_t form_login( + bserv::request_type& request, + bserv::response_type& response, + boost::json::object&& params, + std::shared_ptr conn, + std::shared_ptr session_ptr) { + lgdebug << params << std::endl; + auto context = user_login(request, std::move(params), conn, session_ptr); + lginfo << "login: " << context << std::endl; + return index("index.html", session_ptr, response, context); +} + +std::nullopt_t form_logout( + std::shared_ptr session_ptr, + bserv::response_type& response) { + auto context = user_logout(session_ptr); + lginfo << "logout: " << context << std::endl; + return index("index.html", session_ptr, response, context); +} + +std::nullopt_t redirect_to_users( + std::shared_ptr conn, + std::shared_ptr session_ptr, + bserv::response_type& response, + int page_id, + boost::json::object&& context) { + lgdebug << "view users: " << page_id << std::endl; + bserv::db_transaction tx{ conn }; + bserv::db_result db_res = tx.exec("select count(*) from auth_user;"); + lginfo << db_res.query(); + std::size_t total_users = (*db_res.begin())[0].as(); + lgdebug << "total users: " << total_users << std::endl; + int total_pages = (int)total_users / 10; + if (total_users % 10 != 0) ++total_pages; + lgdebug << "total pages: " << total_pages << std::endl; + db_res = tx.exec("select * from auth_user limit 10 offset ?;", (page_id - 1) * 10); + lginfo << db_res.query(); + auto users = orm_user.convert_to_vector(db_res); + boost::json::array json_users; + for (auto& user : users) { + json_users.push_back(user); + } + boost::json::object pagination; + if (total_pages != 0) { + pagination["total"] = total_pages; + if (page_id > 1) { + pagination["previous"] = page_id - 1; + } + if (page_id < total_pages) { + pagination["next"] = page_id + 1; + } + int lower = page_id - 3; + int upper = page_id + 3; + if (page_id - 3 > 2) { + pagination["left_ellipsis"] = true; + } + else { + lower = 1; + } + if (page_id + 3 < total_pages - 1) { + pagination["right_ellipsis"] = true; + } + else { + upper = total_pages; + } + pagination["current"] = page_id; + boost::json::array pages_left; + for (int i = lower; i < page_id; ++i) { + pages_left.push_back(i); + } + pagination["pages_left"] = pages_left; + boost::json::array pages_right; + for (int i = page_id + 1; i <= upper; ++i) { + pages_right.push_back(i); + } + pagination["pages_right"] = pages_right; + } + context["users"] = json_users; + context["pagination"] = pagination; + return index("users.html", session_ptr, response, context); +} + +std::nullopt_t view_users( + std::shared_ptr conn, + std::shared_ptr session_ptr, + bserv::response_type& response, + const std::string& page_num) { + int page_id = std::stoi(page_num); + boost::json::object context; + return redirect_to_users(conn, session_ptr, response, page_id, std::move(context)); +} + +std::nullopt_t form_add_user( + bserv::request_type& request, + bserv::response_type& response, + boost::json::object&& params, + std::shared_ptr conn, + std::shared_ptr session_ptr) { + boost::json::object context = user_register(request, std::move(params), conn); + return redirect_to_users(conn, session_ptr, response, 1, std::move(context)); +} diff --git a/WebApp/WebApp/handlers.h b/WebApp/WebApp/handlers.h new file mode 100644 index 0000000..87d4652 --- /dev/null +++ b/WebApp/WebApp/handlers.h @@ -0,0 +1,76 @@ +#pragma once + +#include + +#include +#include +#include + +#include "bserv/common.hpp" + +std::nullopt_t hello( + bserv::response_type& response, + std::shared_ptr session_ptr); + +boost::json::object user_register( + bserv::request_type& request, + boost::json::object&& params, + std::shared_ptr conn); + +boost::json::object user_login( + bserv::request_type& request, + boost::json::object&& params, + std::shared_ptr conn, + std::shared_ptr session_ptr); + +boost::json::object find_user( + std::shared_ptr conn, + const std::string& username); + +boost::json::object user_logout( + std::shared_ptr session_ptr); + +boost::json::object send_request( + std::shared_ptr session, + std::shared_ptr client_ptr, + boost::json::object&& params); + +boost::json::object echo( + boost::json::object&& params); + +// websocket +std::nullopt_t ws_echo( + std::shared_ptr session, + std::shared_ptr ws_server); + +std::nullopt_t serve_static_files( + bserv::response_type& response, + const std::string& path); + +std::nullopt_t index( + std::shared_ptr session_ptr, + bserv::response_type& response); + +std::nullopt_t form_login( + bserv::request_type& request, + bserv::response_type& response, + boost::json::object&& params, + std::shared_ptr conn, + std::shared_ptr session_ptr); + +std::nullopt_t form_logout( + std::shared_ptr session_ptr, + bserv::response_type& response); + +std::nullopt_t view_users( + std::shared_ptr conn, + std::shared_ptr session_ptr, + bserv::response_type& response, + const std::string& page_num); + +std::nullopt_t form_add_user( + bserv::request_type& request, + bserv::response_type& response, + boost::json::object&& params, + std::shared_ptr conn, + std::shared_ptr session_ptr); diff --git a/WebApp/WebApp/rendering.cpp b/WebApp/WebApp/rendering.cpp new file mode 100644 index 0000000..79bae51 --- /dev/null +++ b/WebApp/WebApp/rendering.cpp @@ -0,0 +1,84 @@ +#include "rendering.h" + +#include + +#include +#include + +std::string template_root_; +std::string static_root_; +inja::Environment env; + +void init_rendering(const std::string& template_root) { + template_root_ = template_root; + if (template_root_[template_root_.size() - 1] != '/') + template_root_.push_back('/'); +} + +void init_static_root(const std::string& static_root) { + static_root_ = static_root; + if (static_root_[static_root_.size() - 1] != '/') + static_root_.push_back('/'); +} + +std::nullopt_t render( + bserv::response_type& response, + const std::string& template_file, + const boost::json::object& context) { + response.set(bserv::http::field::content_type, "text/html"); + inja::json data = inja::json::parse(boost::json::serialize(context)); + response.body() = env.render_file(template_root_ + template_file, data); + response.prepare_payload(); + return std::nullopt; +} + +// Return a reasonable mime type based on the extension of a file. +boost::beast::string_view +mime_type(boost::beast::string_view path) { + using boost::beast::iequals; + auto const ext = [&path] { + auto const pos = path.rfind("."); + if (pos == boost::beast::string_view::npos) + return boost::beast::string_view{}; + return path.substr(pos); + }(); + if (iequals(ext, ".htm")) return "text/html"; + if (iequals(ext, ".html")) return "text/html"; + if (iequals(ext, ".php")) return "text/html"; + if (iequals(ext, ".css")) return "text/css"; + if (iequals(ext, ".txt")) return "text/plain"; + if (iequals(ext, ".js")) return "application/javascript"; + if (iequals(ext, ".json")) return "application/json"; + if (iequals(ext, ".xml")) return "application/xml"; + if (iequals(ext, ".swf")) return "application/x-shockwave-flash"; + if (iequals(ext, ".flv")) return "video/x-flv"; + if (iequals(ext, ".png")) return "image/png"; + if (iequals(ext, ".jpe")) return "image/jpeg"; + if (iequals(ext, ".jpeg")) return "image/jpeg"; + if (iequals(ext, ".jpg")) return "image/jpeg"; + if (iequals(ext, ".gif")) return "image/gif"; + if (iequals(ext, ".bmp")) return "image/bmp"; + if (iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; + if (iequals(ext, ".tiff")) return "image/tiff"; + if (iequals(ext, ".tif")) return "image/tiff"; + if (iequals(ext, ".svg")) return "image/svg+xml"; + if (iequals(ext, ".svgz")) return "image/svg+xml"; + return "application/text"; +} + +std::string read_bin(const std::string& file) { + std::ifstream fin(file, std::ios_base::in | std::ios_base::binary); + std::string res; + char c; + while ((c = (char)fin.get()) != EOF) res += c; + return res; +} + +std::nullopt_t serve( + bserv::response_type& response, + const std::string& file) { + response.set(bserv::http::field::content_type, mime_type(file)); + response.body() = read_bin(static_root_ + file); + response.prepare_payload(); + return std::nullopt; +} diff --git a/WebApp/WebApp/rendering.h b/WebApp/WebApp/rendering.h new file mode 100644 index 0000000..e7b7ab8 --- /dev/null +++ b/WebApp/WebApp/rendering.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include +#include "bserv/common.hpp" + +void init_rendering(const std::string& template_root); + +void init_static_root(const std::string& static_root); + +std::string read_bin(const std::string& file); + +std::nullopt_t render( + bserv::response_type& response, + const std::string& template_path, + const boost::json::object& context = {} +); + +std::nullopt_t serve( + bserv::response_type& response, + const std::string& file +); diff --git a/WebApp/bserv/bserv.cpp b/WebApp/bserv/bserv.cpp new file mode 100644 index 0000000..732c1bd --- /dev/null +++ b/WebApp/bserv/bserv.cpp @@ -0,0 +1,511 @@ +#include "pch.h" +#include "framework.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bserv/server.hpp" + +#include "bserv/logging.hpp" +#include "bserv/utils.hpp" +#include "bserv/client.hpp" +#include "bserv/websocket.hpp" + +namespace bserv { + + std::string get_address(const tcp::socket& socket) { + tcp::endpoint end_point = socket.remote_endpoint(); + std::string addr = end_point.address().to_string() + + ':' + std::to_string(end_point.port()); + return addr; + } + + http::response handle_request( + http::request& req, router& routes, + std::shared_ptr ws_session, + asio::io_context& ioc, asio::yield_context& yield) { + + const auto bad_request = [&req](beast::string_view why) { + http::response res{ + http::status::bad_request, req.version() }; + res.set(http::field::server, NAME); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = std::string{ why }; + res.prepare_payload(); + return res; + }; + + const auto not_found = [&req](beast::string_view target) { + http::response res{ + http::status::not_found, req.version() }; + res.set(http::field::server, NAME); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "The requested url '" + + std::string{ target } + "' does not exist."; + res.prepare_payload(); + return res; + }; + + const auto server_error = [&req](beast::string_view what) { + http::response res{ + http::status::internal_server_error, req.version() }; + res.set(http::field::server, NAME); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "Internal server error: " + std::string{ what }; + res.prepare_payload(); + return res; + }; + + boost::string_view target = req.target(); + auto pos = target.find('?'); + boost::string_view url; + if (pos == boost::string_view::npos) url = target; + else url = target.substr(0, pos); + + http::response res{ + http::status::ok, req.version() }; + res.set(http::field::server, NAME); + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + + std::optional val; + try { + val = routes(ioc, yield, ws_session, std::string{ url }, req, res); + } + catch (const url_not_found_exception& /*e*/) { + return not_found(url); + } + catch (const bad_request_exception& /*e*/) { + return bad_request("Request body is not a valid JSON string."); + } + catch (const std::exception& e) { + return server_error(e.what()); + } + catch (...) { + return server_error("Unknown exception."); + } + + if (val.has_value()) { + res.body() = json::serialize(val.value()); + res.prepare_payload(); + } + + return res; + } + + class websocket_session_server; + + void handle_websocket_request( + std::shared_ptr, + std::shared_ptr session, + http::request& req, router& routes, + asio::io_context& ioc, asio::yield_context yield); + + class websocket_session_server + : public std::enable_shared_from_this { + private: + friend websocket_server; + std::string address_; + std::shared_ptr session_; + http::request req_; + router& routes_; + void on_accept(beast::error_code ec) { + if (ec) { + fail(ec, "websocket_session_server accept"); + return; + } + // handles request here + asio::spawn( + session_->ioc_, + std::bind( + &handle_websocket_request, + shared_from_this(), + session_, + std::ref(req_), + std::ref(routes_), + std::ref(session_->ioc_), + std::placeholders::_1)); + } + public: + explicit websocket_session_server( + asio::io_context& ioc, + tcp::socket&& socket, + http::request&& req, + router& routes) + : address_{ get_address(socket) }, + session_{ std::make_shared< + websocket_session>(address_, ioc, std::move(socket)) }, + req_{ std::move(req) }, routes_{ routes } { + lgtrace << "websocket_session_server opened: " << address_; + } + ~websocket_session_server() { + lgtrace << "websocket_session_server closed: " << address_; + } + // starts the asynchronous accept operation + void do_accept() { + // sets suggested timeout settings for the websocket + session_->ws_.set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::server)); + // sets a decorator to change the Server of the handshake + session_->ws_.set_option( + websocket::stream_base::decorator( + [](websocket::response_type& res) { + res.set( + http::field::server, + std::string{ BOOST_BEAST_VERSION_STRING } + " websocket-server"); + })); + // accepts the websocket handshake + session_->ws_.async_accept( + req_, + beast::bind_front_handler( + &websocket_session_server::on_accept, + shared_from_this())); + } + }; + + void handle_websocket_request( + std::shared_ptr, + std::shared_ptr session, + http::request& req, router& routes, + asio::io_context& ioc, asio::yield_context yield) { + handle_request(req, routes, session, ioc, yield); + } + + std::string websocket_server::read() { + beast::error_code ec; + beast::flat_buffer buffer; + // reads a message into the buffer + session_.ws_.async_read(buffer, yield_[ec]); + lgtrace << "websocket_server: read from " << session_.address_; + // this indicates that the session was closed + if (ec == websocket::error::closed) { + throw websocket_closed{}; + } + if (ec) { + fail(ec, "websocket_server read"); + throw websocket_io_exception{ "websocket_server read: " + ec.message() }; + } + // lgtrace << "websocket_server: received text? " << ws_.got_text() << " from " << address_; + return beast::buffers_to_string(buffer.data()); + } + + void websocket_server::write(const std::string& data) { + beast::error_code ec; + // ws_.text(ws_.got_text()); + session_.ws_.async_write(asio::buffer(data), yield_[ec]); + lgtrace << "websocket_server: write to " << session_.address_; + if (ec) { + fail(ec, "websocket_server write"); + throw websocket_io_exception{ "websocket_server write: " + ec.message() }; + } + } + + + class http_session; + + // this function produces an HTTP response for the given + // request. The type of the response object depends on the + // contents of the request, so the interface requires the + // caller to pass a generic lambda for receiving the response. + // NOTE: `send` should be called only once! + template + void handle_http_request( + std::shared_ptr, + http::request req, + Send& send, router& routes, asio::io_context& ioc, asio::yield_context yield) { + send(handle_request(req, routes, nullptr, ioc, yield)); + } + + // handles an HTTP server connection + class http_session + : public std::enable_shared_from_this { + private: + // the function object is used to send an HTTP message. + class send_lambda { + private: + http_session& self_; + public: + send_lambda(http_session& self) + : self_{ self } {} + template + void operator()( + http::message&& msg) const { + // the lifetime of the message has to extend + // for the duration of the async operation so + // we use a shared_ptr to manage it. + auto sp = std::make_shared< + http::message>( + std::move(msg)); + // stores a type-erased version of the shared + // pointer in the class to keep it alive. + self_.res_ = sp; + // writes the response + http::async_write( + self_.stream_, *sp, + beast::bind_front_handler( + &http_session::on_write, + self_.shared_from_this(), + sp->need_eof())); + } + } lambda_; + asio::io_context& ioc_; + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + boost::optional< + http::request_parser> parser_; + std::shared_ptr res_; + router& routes_; + router& ws_routes_; + const std::string address_; + void do_read() { + // constructs a new parser for each message + parser_.emplace(); + // applies a reasonable limit to the allowed size + // of the body in bytes to prevent abuse. + parser_->body_limit(PAYLOAD_LIMIT); + // sets the timeout. + stream_.expires_after(std::chrono::seconds(EXPIRY_TIME)); + // reads a request using the parser-oriented interface + http::async_read( + stream_, buffer_, *parser_, + beast::bind_front_handler( + &http_session::on_read, + shared_from_this())); + } + void on_read( + beast::error_code ec, + std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + lgtrace << "received " << bytes_transferred << " byte(s) from: " << address_; + // this means they closed the connection + if (ec == http::error::end_of_stream) { + do_close(); + return; + } + if (ec) { + fail(ec, "http_session async_read"); + return; + } + + // sees if it is a websocket upgrade + if (websocket::is_upgrade(parser_->get())) { + // creates a websocket session, transferring ownership + // of both the socket and the http request + std::make_shared( + ioc_, + stream_.release_socket(), + parser_->release(), + ws_routes_ + )->do_accept(); + return; + } + + // handles the request and sends the response + + asio::spawn( + ioc_, + std::bind( + &handle_http_request, + shared_from_this(), + parser_->release(), + std::ref(lambda_), + std::ref(routes_), + std::ref(ioc_), + std::placeholders::_1)); + // handle_request(parser_->release(), lambda_, routes_); + + // at this point the parser can be reset + } + void on_write( + bool close, beast::error_code ec, + std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + // we're done with the response so delete it + res_.reset(); + if (ec) { + fail(ec, "http_session async_write"); + return; + } + lgtrace << "sent " << bytes_transferred << " byte(s) to: " << address_; + if (close) { + // this means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + do_close(); + return; + } + // reads another request + do_read(); + } + void do_close() { + // sends a TCP shutdown + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); + // at this point the connection is closed gracefully + lgtrace << "socket connection closed: " << address_; + } + public: + http_session( + asio::io_context& ioc, + tcp::socket&& socket, + router& routes, + router& ws_routes) + : lambda_{ *this }, + ioc_{ ioc }, + stream_{ std::move(socket) }, + routes_{ routes }, + ws_routes_{ ws_routes }, + address_{ get_address(stream_.socket()) } { + lgtrace << "http session opened: " << address_; + } + ~http_session() { + lgtrace << "http session closed: " << address_; + } + void run() { + asio::dispatch( + stream_.get_executor(), + beast::bind_front_handler( + &http_session::do_read, + shared_from_this())); + } + }; + + // accepts incoming connections and launches the sessions + class listener + : public std::enable_shared_from_this { + private: + asio::io_context& ioc_; + tcp::acceptor acceptor_; + router& routes_; + router& ws_routes_; + void do_accept() { + acceptor_.async_accept( + asio::make_strand(ioc_), + beast::bind_front_handler( + &listener::on_accept, + shared_from_this())); + } + void on_accept(beast::error_code ec, tcp::socket socket) { + if (ec) { + fail(ec, "listener::acceptor async_accept"); + } + else { + lgtrace << "listener accepts: " << get_address(socket); + std::make_shared( + ioc_, std::move(socket), routes_, ws_routes_)->run(); + } + do_accept(); + } + public: + listener( + asio::io_context& ioc, + tcp::endpoint endpoint, + router& routes, + router& ws_routes) + : ioc_{ ioc }, + acceptor_{ asio::make_strand(ioc) }, + routes_{ routes }, + ws_routes_{ ws_routes } { + beast::error_code ec; + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + fail(ec, "listener::acceptor open"); + exit(EXIT_FAILURE); + return; + } + acceptor_.set_option( + asio::socket_base::reuse_address(true), ec); + if (ec) { + fail(ec, "listener::acceptor set_option"); + exit(EXIT_FAILURE); + return; + } + acceptor_.bind(endpoint, ec); + if (ec) { + fail(ec, "listener::acceptor bind"); + exit(EXIT_FAILURE); + return; + } + acceptor_.listen( + asio::socket_base::max_listen_connections, ec); + if (ec) { + fail(ec, "listener::acceptor listen"); + exit(EXIT_FAILURE); + return; + } + } + void run() { + asio::dispatch( + acceptor_.get_executor(), + beast::bind_front_handler( + &listener::do_accept, + shared_from_this())); + } + }; + + + server::server(const server_config& config, router&& routes, router&& ws_routes) + : ioc_{ config.get_num_threads() }, + routes_{ std::move(routes) }, + ws_routes_{ std::move(ws_routes) } { + init_logging(config); + + // database connection + try { + db_conn_mgr_ = std::make_shared< + db_connection_manager>(config.get_db_conn_str(), config.get_num_db_conn()); + } + catch (const std::exception& e) { + lgfatal << "db connection initialization failed: " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + session_mgr_ = std::make_shared(); + + std::shared_ptr resources_ptr = std::make_shared(); + resources_ptr->session_mgr = session_mgr_; + resources_ptr->db_conn_mgr = db_conn_mgr_; + + routes_.set_resources(resources_ptr); + ws_routes_.set_resources(resources_ptr); + + // creates and launches a listening port + std::make_shared( + ioc_, tcp::endpoint{ tcp::v4(), config.get_port() }, routes_, ws_routes_)->run(); + + // captures SIGINT and SIGTERM to perform a clean shutdown + asio::signal_set signals{ ioc_, SIGINT, SIGTERM }; + signals.async_wait( + [&](const boost::system::error_code&, int) { + // stops the `io_context`. This will cause `run()` + // to return immediately, eventually destroying the + // `io_context` and all of the sockets in it. + ioc_.stop(); + }); + + lginfo << config.get_name() << " started"; + + // runs the I/O service on the requested number of threads + std::vector v; + v.reserve(config.get_num_threads() - 1); + for (int i = 1; i < config.get_num_threads(); ++i) + v.emplace_back([&] { ioc_.run(); }); + ioc_.run(); + + // if we get here, it means we got a SIGINT or SIGTERM + lginfo << "exiting " << config.get_name(); + + // blocks until all the threads exit + for (auto& t : v) t.join(); + } + +} // bserv diff --git a/WebApp/bserv/bserv.vcxproj b/WebApp/bserv/bserv.vcxproj new file mode 100644 index 0000000..2055f2b --- /dev/null +++ b/WebApp/bserv/bserv.vcxproj @@ -0,0 +1,190 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {f5c0cf6d-7bf9-40a5-af2e-8fc36a1d7296} + bserv + 10.0 + + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + C:\Users\jiesh\Desktop\projects\bserv\include;include;%(AdditionalIncludeDirectories) + stdcpp17 + + + + + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + + + + + true + true + true + + + + + Level3 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + ..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;include;%(AdditionalIncludeDirectories) + stdcpp20 + /bigobj %(AdditionalOptions) + + + + + true + + + + + Level3 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp20 + ..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;include;%(AdditionalIncludeDirectories) + + + + + true + true + true + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + \ No newline at end of file diff --git a/WebApp/bserv/bserv.vcxproj.filters b/WebApp/bserv/bserv.vcxproj.filters new file mode 100644 index 0000000..4fed43b --- /dev/null +++ b/WebApp/bserv/bserv.vcxproj.filters @@ -0,0 +1,75 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + + + 源文件 + + + 源文件 + + + 源文件 + + + 源文件 + + + 源文件 + + + 源文件 + + + \ No newline at end of file diff --git a/WebApp/bserv/bserv.vcxproj.user b/WebApp/bserv/bserv.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/WebApp/bserv/bserv.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/WebApp/bserv/client.cpp b/WebApp/bserv/client.cpp new file mode 100644 index 0000000..60659f4 --- /dev/null +++ b/WebApp/bserv/client.cpp @@ -0,0 +1,75 @@ +#include "pch.h" +#include "bserv/client.hpp" +#include "bserv/logging.hpp" + +#include + +namespace bserv { + + // https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/client/async/http_client_async.cpp + // https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/client/coro/http_client_coro.cpp + + // sends one async request to a remote server + http::response http_client_send( + asio::io_context& ioc, + asio::yield_context& yield, + const std::string& host, + const std::string& port, + const http::request& req) { + beast::error_code ec; + tcp::resolver resolver{ ioc }; + const auto results = resolver.async_resolve(host, port, yield[ec]); + if (ec) { + throw request_failed_exception{ "http_client_session::resolver resolve: " + ec.message() }; + } + beast::tcp_stream stream{ ioc }; + // sets a timeout on the operation + stream.expires_after(std::chrono::seconds(EXPIRY_TIME)); + // makes the connection on the IP address we get from a lookup + stream.async_connect(results, yield[ec]); + if (ec) { + throw request_failed_exception{ "http_client_session::stream connect: " + ec.message() }; + } + // sets a timeout on the operation + stream.expires_after(std::chrono::seconds(EXPIRY_TIME)); + // sends the HTTP request to the remote host + http::async_write(stream, req, yield[ec]); + if (ec) { + throw request_failed_exception{ "http_client_session::stream write: " + ec.message() }; + } + beast::flat_buffer buffer; + http::response res; + // receives the HTTP response + http::async_read(stream, buffer, res, yield[ec]); + if (ec) { + throw request_failed_exception{ "http_client_session::stream read: " + ec.message() }; + } + // gracefully close the socket + stream.socket().shutdown(tcp::socket::shutdown_both, ec); + // `not_connected` happens sometimes so don't bother reporting it + if (ec && ec != beast::errc::not_connected) { + // reports the error to the log! + fail(ec, "http_client_session::stream::socket shutdown"); + // return; + } + // if we get here then the connection is closed gracefully + return res; + } + + request_type get_request( + const std::string& host, + const std::string& target, + const http::verb& method, + const boost::json::value& val) { + request_type req; + req.method(method); + req.target(target); + req.set(http::field::host, host); + req.set(http::field::user_agent, NAME); + req.set(http::field::content_type, "application/json"); + req.body() = boost::json::serialize(val); + req.prepare_payload(); + return req; + } + +} // bserv diff --git a/WebApp/bserv/database.cpp b/WebApp/bserv/database.cpp new file mode 100644 index 0000000..0b06ac9 --- /dev/null +++ b/WebApp/bserv/database.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "bserv/database.hpp" + +namespace bserv { + + std::shared_ptr db_connection_manager::get_or_block() { + // `counter_lock_` must be acquired first. + // exchanging this statement with the next will cause dead-lock, + // because if the request is blocked by `counter_lock_`, + // the destructor of `db_connection` will not be able to put + // itself back due to the `queue_lock_` has already been acquired + // by this request! + counter_lock_.lock(); + // `queue_lock_` is acquired so that only one thread will + // modify the `queue_` + std::lock_guard lg{ queue_lock_ }; + std::shared_ptr conn = queue_.front(); + queue_.pop(); + // if there are no connections in the `queue_`, + // `counter_lock_` remains to be locked + // so that the following requests will be blocked + if (queue_.size() != 0) counter_lock_.unlock(); + return std::make_shared(*this, conn); + } + + db_connection::~db_connection() { + std::lock_guard lg{ mgr_.queue_lock_ }; + mgr_.queue_.emplace(conn_); + // if this is the first available connection back to the queue, + // `counter_lock_` is unlocked so that the blocked requests will + // be notified + if (mgr_.queue_.size() == 1) + mgr_.counter_lock_.unlock(); + } + +} // bserv \ No newline at end of file diff --git a/WebApp/bserv/framework.h b/WebApp/bserv/framework.h new file mode 100644 index 0000000..d2d95bd --- /dev/null +++ b/WebApp/bserv/framework.h @@ -0,0 +1,3 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容 diff --git a/WebApp/bserv/include/bserv/client.hpp b/WebApp/bserv/include/bserv/client.hpp new file mode 100644 index 0000000..e290831 --- /dev/null +++ b/WebApp/bserv/include/bserv/client.hpp @@ -0,0 +1,145 @@ +#ifndef _CLIENT_HPP +#define _CLIENT_HPP + +#include +#include +#include +#include + +#include +#include +#include + +namespace bserv { + + namespace beast = boost::beast; + namespace http = beast::http; + namespace asio = boost::asio; + namespace json = boost::json; + using asio::ip::tcp; + + using request_type = http::request; + using response_type = http::response; + + class request_failed_exception + : public std::exception { + private: + const std::string msg_; + public: + request_failed_exception(const std::string& msg) : msg_{ msg } {} + const char* what() const noexcept { return msg_.c_str(); } + }; + + http::response http_client_send( + asio::io_context& ioc, + asio::yield_context& yield, + const std::string& host, + const std::string& port, + const http::request& req); + + request_type get_request( + const std::string& host, + const std::string& target, + const http::verb& method, + const boost::json::value& val); + + class http_client { + private: + asio::io_context& ioc_; + asio::yield_context& yield_; + public: + http_client(asio::io_context& ioc, asio::yield_context& yield) + : ioc_{ ioc }, yield_{ yield } {} + http::response request( + const std::string& host, + const std::string& port, + const http::request& req) { + return http_client_send(ioc_, yield_, host, port, req); + } + boost::json::value request_for_value( + const std::string& host, + const std::string& port, + const http::request& req) { + return boost::json::parse(request(host, port, req).body()); + } + + response_type send( + const std::string& host, + const std::string& port, + const std::string& target, + const http::verb& method, + const boost::json::value& val) { + request_type req = get_request(host, target, method, val); + return request(host, port, req); + } + boost::json::value send_for_value( + const std::string& host, + const std::string& port, + const std::string& target, + const http::verb& method, + const boost::json::value& val) { + request_type req = get_request(host, target, method, val); + return request_for_value(host, port, req); + } + + response_type get( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send(host, port, target, http::verb::get, val); + } + boost::json::value get_for_value( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send_for_value(host, port, target, http::verb::get, val); + } + response_type put( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send(host, port, target, http::verb::put, val); + } + boost::json::value put_for_value( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send_for_value(host, port, target, http::verb::put, val); + } + response_type post( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send(host, port, target, http::verb::post, val); + } + boost::json::value post_for_value( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send_for_value(host, port, target, http::verb::post, val); + } + response_type delete_( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send(host, port, target, http::verb::delete_, val); + } + boost::json::value delete_for_value( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::value& val) { + return send_for_value(host, port, target, http::verb::delete_, val); + } + }; + +} // bserv + +#endif // _CLIENT_HPP \ No newline at end of file diff --git a/bserv/common.hpp b/WebApp/bserv/include/bserv/common.hpp similarity index 82% rename from bserv/common.hpp rename to WebApp/bserv/include/bserv/common.hpp index 27bc7e6..1c1cfdf 100644 --- a/bserv/common.hpp +++ b/WebApp/bserv/include/bserv/common.hpp @@ -1,6 +1,8 @@ #ifndef _COMMON_HPP #define _COMMON_HPP +#define _WIN32_WINNT 0x0601 + #include "client.hpp" #include "config.hpp" #include "database.hpp" diff --git a/WebApp/bserv/include/bserv/config.hpp b/WebApp/bserv/include/bserv/config.hpp new file mode 100644 index 0000000..a0bf656 --- /dev/null +++ b/WebApp/bserv/include/bserv/config.hpp @@ -0,0 +1,50 @@ +#ifndef _CONFIG_HPP +#define _CONFIG_HPP + +#include +#include +#include +#include +#include + +namespace bserv { + + const std::string NAME = "bserv"; + + const unsigned short PORT = 8080; + const int NUM_THREADS = + std::thread::hardware_concurrency() > 0 ? std::thread::hardware_concurrency() : 1; + + const std::size_t PAYLOAD_LIMIT = 8 * 1024 * 1024; + const int EXPIRY_TIME = 30; // seconds + + const std::size_t LOG_ROTATION_SIZE = 8 * 1024 * 1024; + const std::string LOG_PATH = "./log/" + NAME; + + const int NUM_DB_CONN = 10; + const std::string DB_CONN_STR = "dbname=bserv"; + +#define decl_field(type, name, default_value) \ +private: \ + std::optional name##_; \ +public: \ + void set_##name(std::optional&& name) { name##_ = std::move(name); } \ + type get_##name() const { return name##_.has_value() ? name##_.value() : default_value; } + + struct server_config { + decl_field(std::string, name, NAME) + decl_field(unsigned short, port, PORT) + decl_field(int, num_threads, NUM_THREADS) + decl_field(std::size_t, log_rotation_size, LOG_ROTATION_SIZE) + decl_field(std::string, log_path, LOG_PATH) + decl_field(int, num_db_conn, NUM_DB_CONN) + decl_field(std::string, db_conn_str, DB_CONN_STR) + public: + server_config() = default; + }; + +#undef decl_field + +} // bserv + +#endif // _CONFIG_HPP \ No newline at end of file diff --git a/WebApp/bserv/include/bserv/database.hpp b/WebApp/bserv/include/bserv/database.hpp new file mode 100644 index 0000000..f7b88f6 --- /dev/null +++ b/WebApp/bserv/include/bserv/database.hpp @@ -0,0 +1,355 @@ +#ifndef _DATABASE_HPP +#define _DATABASE_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace bserv { + + using raw_db_connection_type = pqxx::connection; + using raw_db_transaction_type = pqxx::work; + + class db_field { + private: + pqxx::field field_; + public: + db_field(const pqxx::field& field) : field_{ field } {} + const char* c_str() const { return field_.c_str(); } + template + Type as() const { return field_.as(); } + }; + + class db_row { + private: + pqxx::row row_; + public: + db_row(const pqxx::row& row) : row_{ row } {} + std::size_t size() const { return row_.size(); } + db_field operator[](std::size_t idx) const { return row_[(pqxx::row::size_type)idx]; } + }; + + class db_result { + private: + pqxx::result result_; + public: + class const_iterator { + private: + pqxx::result::const_iterator iterator_; + public: + const_iterator( + const pqxx::result::const_iterator& iterator + ) : iterator_{ iterator } {} + const_iterator& operator++() { ++iterator_; return *this; } + bool operator==(const const_iterator& rhs) const { return iterator_ == rhs.iterator_; } + bool operator!=(const const_iterator& rhs) const { return iterator_ != rhs.iterator_; } + db_row operator*() const { return *iterator_; } + }; + db_result() = default; + db_result(const pqxx::result& result) : result_{ result } {} + const_iterator begin() const { return result_.begin(); } + const_iterator end() const { return result_.end(); } + std::string query() const { return result_.query(); } + }; + + class db_connection_manager; + + class db_connection { + private: + db_connection_manager& mgr_; + std::shared_ptr conn_; + public: + db_connection( + db_connection_manager& mgr, + std::shared_ptr conn) + : mgr_{ mgr }, conn_{ conn } {} + // non-copiable, non-assignable + db_connection(const db_connection&) = delete; + db_connection& operator=(const db_connection&) = delete; + // during the destruction, it should put itself back to the + // manager's queue + ~db_connection(); + raw_db_connection_type& get() { return *conn_; } + }; + + // provides the database connection pool functionality + class db_connection_manager { + private: + std::queue> queue_; + // this lock is for manipulating the `queue_` + mutable std::mutex queue_lock_; + // since C++ 17 doesn't provide the semaphore functionality, + // mutex is used to mimic it. (boost provides it) + // if there are no available connections, trying to lock on + // it will cause blocking. + mutable std::mutex counter_lock_; + friend db_connection; + public: + db_connection_manager(const std::string& conn_str, int n) { + for (int i = 0; i < n; ++i) + queue_.emplace( + std::make_shared(conn_str)); + } + // if there are no available database connections, this function + // blocks until there is any; + // otherwise, this function returns a pointer to `db_connection`. + std::shared_ptr get_or_block(); + }; + + // ************************************************************************** + + class db_parameter { + public: + virtual ~db_parameter() = default; + virtual std::string get_value(raw_db_transaction_type&) = 0; + }; + + class db_name : public db_parameter { + private: + std::string value_; + public: + db_name(const std::string& value) + : value_{ value } {} + std::string get_value(raw_db_transaction_type& tx) { + return tx.quote_name(value_); + } + }; + + template + class db_value : public db_parameter { + private: + Type value_; + public: + db_value(const Type& value) + : value_{ value } {} + std::string get_value(raw_db_transaction_type&) { + return std::to_string(value_); + } + }; + + template <> + class db_value : public db_parameter { + private: + std::string value_; + public: + db_value(const std::string& value) + : value_{ value } {} + std::string get_value(raw_db_transaction_type& tx) { + return tx.quote(value_); + } + }; + + template <> + class db_value : public db_parameter { + private: + bool value_; + public: + db_value(const bool& value) + : value_{ value } {} + std::string get_value(raw_db_transaction_type&) { + return value_ ? "true" : "false"; + } + }; + + template + class db_value> : public db_parameter { + private: + std::vector value_; + public: + db_value(const std::vector& value) + : value_{ value } {} + std::string get_value(raw_db_transaction_type& tx) { + std::string res; + for (const auto& elem : value_) { + if (res.size() != 0) res += ", "; + res += db_value{elem}.get_value(tx); + } + return "ARRAY[" + res + "]"; + } + }; + + namespace db_internal { + + template + std::shared_ptr convert_parameter( + const Param& param) { + return std::make_shared>(param); + } + + template + std::shared_ptr convert_parameter( + const db_value& param) { + return std::make_shared>(param); + } + + inline std::shared_ptr convert_parameter( + const char* param) { + return std::make_shared>(param); + } + + inline std::shared_ptr convert_parameter( + const db_name& param) { + return std::make_shared(param); + } + + template + std::vector convert_parameters( + raw_db_transaction_type& tx, std::shared_ptr... params) { + return { params->get_value(tx)... }; + } + + // ************************************* + + class db_field_holder { + protected: + std::string name_; + public: + db_field_holder(const std::string& name) + : name_{ name } {} + virtual ~db_field_holder() = default; + virtual void add( + const db_row& row, std::size_t field_idx, + boost::json::object& obj) = 0; + }; + + template + class db_field : public db_field_holder { + public: + using db_field_holder::db_field_holder; + void add( + const db_row& row, std::size_t field_idx, + boost::json::object& obj) { + obj[name_] = row[field_idx].as(); + } + }; + + template <> + class db_field : public db_field_holder { + public: + using db_field_holder::db_field_holder; + void add( + const db_row& row, std::size_t field_idx, + boost::json::object& obj) { + obj[name_] = row[field_idx].c_str(); + } + }; + + } // db_internal + + template + std::shared_ptr make_db_field( + const std::string& name) { + return std::make_shared>(name); + } + + class invalid_operation_exception : public std::exception { + private: + std::string msg_; + public: + invalid_operation_exception(const std::string& msg) + : msg_{ msg } {} + const char* what() const noexcept { return msg_.c_str(); } + }; + + class db_relation_to_object { + private: + std::vector> fields_; + public: + db_relation_to_object( + const std::initializer_list< + std::shared_ptr>&fields) + : fields_{ fields } {} + boost::json::object convert_row(const db_row& row) { + boost::json::object obj; + for (std::size_t i = 0; i < fields_.size(); ++i) + fields_[i]->add(row, i, obj); + return obj; + } + std::vector convert_to_vector( + const db_result& result) { + std::vector results; + for (const auto& row : result) + results.emplace_back(convert_row(row)); + return results; + } + std::optional convert_to_optional( + const db_result& result) { + // result.size() == 0 + if (result.begin() == result.end()) return std::nullopt; + auto iterator = result.begin(); + auto first = iterator; + // result.size() == 1 + if (++iterator == result.end()) + return convert_row(*first); + // result.size() > 1 + throw invalid_operation_exception{ + "too many objects to convert" }; + } + }; + + class db_transaction { + private: + raw_db_transaction_type tx_; + public: + db_transaction( + std::shared_ptr connection_ptr + ) : tx_{ connection_ptr->get() } {} + // non-copiable, non-assignable + db_transaction(const db_transaction&) = delete; + db_transaction& operator=(const db_transaction&) = delete; + // Usage: + // exec("select * from ? where ? = ? and first_name = 'Name??'", + // db_name("auth_user"), db_name("is_active"), db_value(true)); + // -> SQL: select * from "auth_user" where "is_active" = true and first_name = 'Name?' + // ====================================================================================== + // exec("select * from ? where ? = ? and first_name = ?", + // db_name("auth_user"), db_name("is_active"), false, "Name??"); + // -> SQL: select * from "auth_user" where "is_active" = false and first_name = 'Name??' + // ====================================================================================== + // Note: "?" is the placeholder for parameters, and "??" will be converted to "?" in SQL. + // But, "??" in the parameters remains. + template + db_result exec(const std::string& s, const Params&... params) { + std::vector param_vec = + db_internal::convert_parameters( + tx_, db_internal::convert_parameter(params)...); + std::size_t idx = 0; + std::string query; + for (std::size_t i = 0; i < s.length(); ++i) { + if (s[i] == '?') { + if (i + 1 < s.length() && s[i + 1] == '?') { + query += s[++i]; + } + else { + if (idx < param_vec.size()) { + query += param_vec[idx++]; + } + else throw std::out_of_range{ "too few parameters" }; + } + } + else query += s[i]; + } + if (idx != param_vec.size()) + throw invalid_operation_exception{ "too many parameters" }; + return tx_.exec(query); + } + void commit() { tx_.commit(); } + void abort() { tx_.abort(); } + }; + + + // TODO: add support for time conversions between postgresql and c++, use timestamp? + // what about time zone? + +} // bserv + +#endif // _DATABASE_HPP \ No newline at end of file diff --git a/WebApp/bserv/include/bserv/logging.hpp b/WebApp/bserv/include/bserv/logging.hpp new file mode 100644 index 0000000..fee95c1 --- /dev/null +++ b/WebApp/bserv/include/bserv/logging.hpp @@ -0,0 +1,57 @@ +#ifndef _LOGGING_HPP +#define _LOGGING_HPP + +//#define BOOST_LOG_DYN_LINK + +#include +#include +#include +#include + +#include +#include +#include + +#include "config.hpp" + +#define lgtrace BOOST_LOG_TRIVIAL(trace) +#define lgdebug BOOST_LOG_TRIVIAL(debug) +#define lginfo BOOST_LOG_TRIVIAL(info) +#define lgwarning BOOST_LOG_TRIVIAL(warning) +#define lgerror BOOST_LOG_TRIVIAL(error) +#define lgfatal BOOST_LOG_TRIVIAL(fatal) + +namespace bserv { + + namespace logging = boost::log; + namespace keywords = boost::log::keywords; + namespace src = boost::log::sources; + + // this function should be called before logging is used + inline void init_logging(const server_config& config) { + logging::add_file_log( + keywords::file_name = config.get_log_path() + "_%Y%m%d_%H-%M-%S.%N.log", + keywords::rotation_size = config.get_log_rotation_size(), + keywords::format = "[%Severity%][%TimeStamp%][%ThreadID%]: %Message%" + ); +#if defined(_MSC_VER) && defined(_DEBUG) + // write to console as well + logging::add_console_log(std::cout); +#endif + logging::core::get()->set_filter( +#if defined(_MSC_VER) && defined(_DEBUG) + logging::trivial::severity >= logging::trivial::trace +#else + logging::trivial::severity >= logging::trivial::info +#endif + ); + logging::add_common_attributes(); + } + + inline void fail(const boost::system::error_code& ec, const char* what) { + lgerror << what << ": " << ec.message() << std::endl; + } + +} // bserv + +#endif // _LOGGING_HPP \ No newline at end of file diff --git a/WebApp/bserv/include/bserv/router.hpp b/WebApp/bserv/include/bserv/router.hpp new file mode 100644 index 0000000..566bc15 --- /dev/null +++ b/WebApp/bserv/include/bserv/router.hpp @@ -0,0 +1,431 @@ +#ifndef _ROUTER_HPP +#define _ROUTER_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "client.hpp" +#include "database.hpp" +#include "session.hpp" +#include "utils.hpp" +#include "config.hpp" +#include "websocket.hpp" + +namespace bserv { + + namespace beast = boost::beast; + namespace http = beast::http; + + struct server_resources { + std::shared_ptr session_mgr; + std::shared_ptr db_conn_mgr; + }; + + struct request_resources { + server_resources& resources; + + asio::io_context& ioc; + asio::yield_context& yield; + std::shared_ptr ws_session; + const std::vector& url_params; + request_type& request; + response_type& response; + + std::shared_ptr session_ptr; + std::shared_ptr db_connection_ptr; + std::shared_ptr http_client_ptr; + std::shared_ptr websocket_server_ptr; + }; + + namespace placeholders { + + template + struct placeholder {}; + +#define make_place_holder(x) constexpr placeholder _##x + + make_place_holder(1); + make_place_holder(2); + make_place_holder(3); + make_place_holder(4); + make_place_holder(5); + make_place_holder(6); + make_place_holder(7); + make_place_holder(8); + make_place_holder(9); + +#undef make_place_holder + + // std::shared_ptr + constexpr placeholder<-1> session; + // bserv::request_type& + constexpr placeholder<-2> request; + // bserv::response_type& + constexpr placeholder<-3> response; + // boost::json::object&& + constexpr placeholder<-4> json_params; + // std::shared_ptr + constexpr placeholder<-5> db_connection_ptr; + // std::shared_ptr + constexpr placeholder<-6> http_client_ptr; + // std::shared_ptr + constexpr placeholder<-7> websocket_server_ptr; + + } // placeholders + + class bad_request_exception : public std::exception { + public: + bad_request_exception() = default; + const char* what() const noexcept { return "bad request"; } + }; + + namespace router_internal { + + template + struct parameter_pack; + + template <> + struct parameter_pack<> {}; + + template + struct parameter_pack + : parameter_pack { + Head head_; + template + parameter_pack(Head2&& head, Tail2&& ...tail) + : parameter_pack{ static_cast(tail)... }, + head_{ static_cast(head) } {} + }; + + template + struct get_parameter_pack; + + template + struct get_parameter_pack + : get_parameter_pack {}; + + template + struct get_parameter_pack<0, Head, Tail...> { + using type = parameter_pack; + }; + + template + decltype(auto) get_parameter_value(parameter_pack& params) { + return (static_cast< + typename get_parameter_pack::type& + >(params).head_); + } + + template + struct get_parameter; + + template + struct get_parameter + : get_parameter {}; + + template + struct get_parameter<0, Head, Tail...> { + using type = Head; + }; + + template + Type&& get_parameter_data( + request_resources&, Type&& val) { + return static_cast(val); + } + + template = 0), int> = 0> + const std::string& get_parameter_data( + request_resources& resources, + placeholders::placeholder) { + return resources.url_params[N]; + } + + inline std::shared_ptr get_parameter_data( + request_resources& resources, + placeholders::placeholder<-1>) { + if (resources.session_ptr != nullptr) + return resources.session_ptr; + std::string cookie_str{ resources.request[http::field::cookie] }; + auto&& [cookie_dict, cookie_list] + = utils::parse_params(cookie_str, 0, ';'); + boost::ignore_unused(cookie_list); + std::string session_id; + if (cookie_dict.count(SESSION_NAME) != 0) { + session_id = cookie_dict[SESSION_NAME]; + } + std::shared_ptr session_ptr; + if (resources.resources.session_mgr->get_or_create(session_id, session_ptr)) { + resources.response.set(http::field::set_cookie, SESSION_NAME + "=" + session_id); + } + resources.session_ptr = session_ptr; + return session_ptr; + } + + inline request_type& get_parameter_data( + request_resources& resources, + placeholders::placeholder<-2>) { + return resources.request; + } + + inline response_type& get_parameter_data( + request_resources& resources, + placeholders::placeholder<-3>) { + return resources.response; + } + + inline boost::json::object get_parameter_data( + request_resources& resources, + placeholders::placeholder<-4>) { + boost::json::object body; + auto add_to_body = [&body]( + const std::map& dict_param, + const std::map>& list_param) { + for (auto& [k, v] : dict_param) { + if (!body.contains(k)) { + body[k] = v; + } + } + for (auto& [k, vs] : list_param) { + if (!body.contains(k)) { + boost::json::array a; + for (auto& v : vs) { + a.push_back(boost::json::string{ v }); + } + body[k] = a; + } + } + }; + if (!resources.request.body().empty()) { + if (resources.request[http::field::content_type] == "application/json") { + try { + body = boost::json::parse(resources.request.body()).as_object(); + } + catch (const std::exception& /*e*/) { + throw bad_request_exception{}; + } + } + else if (resources.request[http::field::content_type] == "application/x-www-form-urlencoded") { + std::string copied_body{ resources.request.body() }; + auto&& [dict_params, list_params] = utils::parse_params(copied_body); + add_to_body(dict_params, list_params); + } + } + std::string target{ resources.request.target() }; + auto&& [url, dict_params, list_params] = utils::parse_url(target); + boost::ignore_unused(url); + add_to_body(dict_params, list_params); + return body; + } + + inline std::shared_ptr get_parameter_data( + request_resources& resources, + placeholders::placeholder<-5>) { + if (resources.db_connection_ptr == nullptr) + resources.db_connection_ptr = + resources.resources.db_conn_mgr->get_or_block(); + return resources.db_connection_ptr; + } + + inline std::shared_ptr get_parameter_data( + request_resources& resources, + placeholders::placeholder<-6>) { + if (resources.http_client_ptr == nullptr) + resources.http_client_ptr = + std::make_shared(resources.ioc, resources.yield); + return resources.http_client_ptr; + } + + inline std::shared_ptr get_parameter_data( + request_resources& resources, + placeholders::placeholder<-7>) { + if (resources.websocket_server_ptr == nullptr) + resources.websocket_server_ptr = + std::make_shared(*resources.ws_session, resources.yield); + return resources.websocket_server_ptr; + } + + template + struct path_handler; + + template + struct path_handler> { + Ret invoke(request_resources& resources, + Ret(*pf)(Args ...), parameter_pack& params) { + // suppress msvc warning + boost::ignore_unused(params); + if constexpr (Idx == 0) return (*pf)(); + else return static_cast, + typename get_parameter::type>* + >(this)->invoke2(resources, pf, params, + get_parameter_data(resources, + get_parameter_value(params))); + } + }; + + template + struct path_handler, Head, Tail...> + : path_handler, Tail...> { + template < + typename Head2, typename ...Tail2, + std::enable_if_t = 0> + Ret invoke2(request_resources& resources, + Ret(*pf)(Args ...), parameter_pack& params, + Head2&& head2, Tail2&& ...tail2) { + // suppress msvc warning + boost::ignore_unused(params); + if constexpr (Idx == 0) + return (*pf)(static_cast(head2), + static_cast(tail2)...); + else return static_cast, + typename get_parameter::type, Head, Tail...>* + >(this)->invoke2(resources, pf, params, + get_parameter_data(resources, + get_parameter_value(params)), + static_cast(head2), static_cast(tail2)...); + } + }; + + const std::vector> url_regex_mapping{ + {std::regex{""}, "([0-9]+)"}, + {std::regex{""}, R"(([A-Za-z0-9_\.\-]+))"}, + {std::regex{""}, R"(([A-Za-z0-9_/\.\-]+))"} + }; + + inline std::string get_re_url(const std::string& url) { + std::string re_url = url; + for (auto& [r, s] : url_regex_mapping) + re_url = std::regex_replace(re_url, r, s); + return re_url; + } + + struct path_holder : std::enable_shared_from_this { + path_holder() = default; + virtual ~path_holder() = default; + virtual bool match( + const std::string&, + std::vector&) const = 0; + virtual std::optional invoke( + request_resources&) = 0; + }; + + template + class path; + + template + class path> + : public path_holder { + private: + std::regex re_; + Ret(*pf_)(Args ...); + parameter_pack params_; + path_handler<0, Ret(*)(Args ...), parameter_pack, Params...> handler_; + public: + path(const std::string& url, Ret(*pf)(Args ...), Params&& ...params) + : re_{ get_re_url(url) }, pf_{ pf }, + params_{ static_cast(params)... } {} + bool match(const std::string& url, std::vector& result) const { + std::smatch r; + bool matched = std::regex_match(url, r, re_); + if (matched) { + result.clear(); + for (auto& sub : r) + result.push_back(sub.str()); + } + return matched; + } + std::optional invoke( + request_resources& resources) { + return handler_.invoke( + resources, pf_, params_); + } + }; + + } // router_internal + + template + std::shared_ptr>> make_path( + const std::string& url, Ret(*pf)(Args ...), Params&& ...params) { + return std::make_shared< + router_internal::path> + >(url, pf, static_cast(params)...); + } + + template + std::shared_ptr>> make_path( + const char* url, Ret(*pf)(Args ...), Params&& ...params) { + return std::make_shared< + router_internal::path> + >(url, pf, static_cast(params)...); + } + + class url_not_found_exception : public std::exception { + public: + url_not_found_exception() = default; + const char* what() const noexcept { return "url not found"; } + }; + + class router { + private: + using path_holder_type = std::shared_ptr; + std::vector paths_; + std::shared_ptr resources_; + public: + router(const std::initializer_list& paths) + : paths_{ paths } {} + void set_resources(std::shared_ptr resources) { + resources_ = resources; + } + std::optional operator()( + asio::io_context& ioc, asio::yield_context& yield, + std::shared_ptr ws_session, + const std::string& url, request_type& request, response_type& response) { + std::vector url_params; + for (auto& ptr : paths_) { + if (ptr->match(url, url_params)) { + request_resources resources{ + *resources_, + + ioc, + yield, + ws_session, + url_params, + request, + response, + + nullptr, + nullptr, + nullptr, + nullptr + }; + return ptr->invoke(resources); + } + } + throw url_not_found_exception{}; + } + }; + +} // bserv + +#endif // _ROUTER_HPP \ No newline at end of file diff --git a/bserv/server.hpp b/WebApp/bserv/include/bserv/server.hpp similarity index 59% rename from bserv/server.hpp rename to WebApp/bserv/include/bserv/server.hpp index 682438f..319dd0d 100644 --- a/bserv/server.hpp +++ b/WebApp/bserv/include/bserv/server.hpp @@ -1,14 +1,14 @@ /** * bserv - Boost-based HTTP Server - * + * * reference: * https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/server/async/http_server_async.cpp * https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/server/coro/http_server_coro.cpp * https://www.boost.org/doc/libs/1_75_0/libs/beast/example/advanced/server/advanced_server.cpp - * + * * websocket: * https://www.boost.org/doc/libs/1_75_0/libs/beast/example/websocket/server/async/websocket_server_async.cpp - * + * */ #ifndef _SERVER_HPP @@ -29,24 +29,24 @@ namespace bserv { -namespace beast = boost::beast; -namespace http = beast::http; -namespace websocket = beast::websocket; -namespace asio = boost::asio; -namespace json = boost::json; -using asio::ip::tcp; + namespace beast = boost::beast; + namespace http = beast::http; + namespace websocket = beast::websocket; + namespace asio = boost::asio; + namespace json = boost::json; + using asio::ip::tcp; -class server { -private: - // io_context for all I/O - asio::io_context ioc_; - router routes_; - router ws_routes_; - std::shared_ptr session_mgr_; - std::shared_ptr db_conn_mgr_; -public: - server(const server_config& config, router&& routes, router&& ws_routes = {}); -}; + class server { + private: + // io_context for all I/O + asio::io_context ioc_; + router routes_; + router ws_routes_; + std::shared_ptr session_mgr_; + std::shared_ptr db_conn_mgr_; + public: + server(const server_config& config, router&& routes, router&& ws_routes = {}); + }; } // bserv diff --git a/WebApp/bserv/include/bserv/session.hpp b/WebApp/bserv/include/bserv/session.hpp new file mode 100644 index 0000000..9d5c502 --- /dev/null +++ b/WebApp/bserv/include/bserv/session.hpp @@ -0,0 +1,69 @@ +#ifndef _SESSION_HPP +#define _SESSION_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.hpp" + +namespace bserv { + + const std::string SESSION_NAME = "bsessionid"; + + // using session_type = std::map; + using session_type = boost::json::object; + + struct session_manager_base + : std::enable_shared_from_this { + virtual ~session_manager_base() = default; + // if `key` refers to an existing session, that session will be placed in + // `session_ptr` and this function will return `false`. + // otherwise, this function will create a new session, place the created + // session in `session_ptr`, place the session id in `key`, and return `true`. + // this means, the returned value indicates whether a new session is created, + // the `session_ptr` will point to a session with `key` as its session id, + // after this function is called. + // NOTE: a `shared_ptr` is returned instead of a reference. + virtual bool get_or_create( + std::string& key, + std::shared_ptr& session_ptr) = 0; + }; + + class memory_session_manager : public session_manager_base { + private: + using time_point = std::chrono::steady_clock::time_point; + std::mt19937 rng_; + std::uniform_int_distribution dist_; + std::map str_to_int_; + std::map int_to_str_; + std::map> sessions_; + // `expiry` stores tuple sorted by key + std::map expiry_; + // `queue` functions as a priority queue + // (the front element is the smallest) + // and stores tuple sorted by + // expiry first and then key. + std::set> queue_; + mutable std::mutex lock_; + public: + memory_session_manager() + : rng_{ utils::internal::get_rd_value() }, + dist_{ 0, std::numeric_limits::max() } {} + bool get_or_create( + std::string& key, + std::shared_ptr& session_ptr); + }; + +} // bserv + +#endif // _SESSION_HPP \ No newline at end of file diff --git a/WebApp/bserv/include/bserv/utils.hpp b/WebApp/bserv/include/bserv/utils.hpp new file mode 100644 index 0000000..32d4b96 --- /dev/null +++ b/WebApp/bserv/include/bserv/utils.hpp @@ -0,0 +1,62 @@ +#ifndef _UTILS_HPP +#define _UTILS_HPP + +#include +#include +#include +#include +#include +#include + +namespace bserv::utils { + + namespace internal { + + std::random_device::result_type get_rd_value(); + + } // internal + + std::string generate_random_string(std::size_t len); + + namespace security { + + bool constant_time_compare(const std::string& a, const std::string& b); + + std::string hash_password( + const std::string& password, + const std::string& salt, + unsigned int iterations = 20000 /*320000*/); + + std::string encode_password(const std::string& password); + + bool check_password(const std::string& password, + const std::string& encoded_password); + + } // security + + // there can be exceptions (std::stoi)! + std::string decode_url(const std::string& s); + + std::string encode_url(const std::string& s); + + // this function parses param list in the form of k1=v1&k2=v2..., + // where '&' can be any delimiter. + // ki and vi will be converted if they are percent-encoded, + // which is why the returned values are `string`, not `string_view`. + std::pair< + std::map, + std::map>> + parse_params(std::string& s, std::size_t start_pos = 0, char delimiter = '&'); + + // this function parses url in the form of [url]?k1=v1&k2=v2... + // this function will convert ki and vi if they are percent-encoded. + // NOTE: don't misuse this function, it's going to modify + // the parameter `s` in place! + std::tuple, + std::map>> + parse_url(std::string& s); + +} // bserv::utils + +#endif // _UTILS_HPP \ No newline at end of file diff --git a/WebApp/bserv/include/bserv/websocket.hpp b/WebApp/bserv/include/bserv/websocket.hpp new file mode 100644 index 0000000..8acf13e --- /dev/null +++ b/WebApp/bserv/include/bserv/websocket.hpp @@ -0,0 +1,65 @@ +#ifndef _WEBSOCKET_HPP +#define _WEBSOCKET_HPP + +#include +#include +#include + +#include +#include +#include +#include + +namespace bserv { + + namespace beast = boost::beast; + namespace http = beast::http; + namespace websocket = beast::websocket; + namespace asio = boost::asio; + namespace json = boost::json; + using asio::ip::tcp; + + class websocket_closed + : public std::exception { + public: + websocket_closed() {} + const char* what() const noexcept { return "websocket session has been closed"; } + }; + + class websocket_io_exception + : public std::exception { + private: + const std::string msg_; + public: + websocket_io_exception(const std::string& msg) : msg_{ msg } {} + const char* what() const noexcept { return msg_.c_str(); } + }; + + struct websocket_session { + const std::string address_; + asio::io_context& ioc_; + websocket::stream ws_; + websocket_session( + const std::string& address, + asio::io_context& ioc, + tcp::socket&& socket) + : address_{ address }, + ioc_{ ioc }, ws_{ std::move(socket) } {} + }; + + class websocket_server { + private: + websocket_session& session_; + asio::yield_context& yield_; + public: + websocket_server(websocket_session& session, asio::yield_context& yield) + : session_{ session }, yield_{ yield } {} + std::string read(); + boost::json::value read_json() { return boost::json::parse(read()); } + void write(const std::string& data); + void write_json(const boost::json::value& val) { write(boost::json::serialize(val)); } + }; + +} // bserv + +#endif // _WEBSOCKET_HPP \ No newline at end of file diff --git a/WebApp/bserv/pch.cpp b/WebApp/bserv/pch.cpp new file mode 100644 index 0000000..b6fb8f4 --- /dev/null +++ b/WebApp/bserv/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: 与预编译标头对应的源文件 + +#include "pch.h" + +// 当使用预编译的头时,需要使用此源文件,编译才能成功。 diff --git a/WebApp/bserv/pch.h b/WebApp/bserv/pch.h new file mode 100644 index 0000000..81f0432 --- /dev/null +++ b/WebApp/bserv/pch.h @@ -0,0 +1,15 @@ +// pch.h: 这是预编译标头文件。 +// 下方列出的文件仅编译一次,提高了将来生成的生成性能。 +// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。 +// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。 +// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。 + +#ifndef PCH_H +#define PCH_H + +// 添加要在此处预编译的标头 +#include "framework.h" + +#define _WIN32_WINNT 0x0601 + +#endif //PCH_H diff --git a/WebApp/bserv/session.cpp b/WebApp/bserv/session.cpp new file mode 100644 index 0000000..2ab42f1 --- /dev/null +++ b/WebApp/bserv/session.cpp @@ -0,0 +1,50 @@ +#include "pch.h" +#include "bserv/session.hpp" + +namespace bserv { + + bool memory_session_manager::get_or_create( + std::string& key, + std::shared_ptr& session_ptr) { + std::lock_guard lg{ lock_ }; + time_point now = std::chrono::steady_clock::now(); + // removes the expired sessions + while (!queue_.empty() && queue_.begin()->first < now) { + std::size_t another_key = queue_.begin()->second; + sessions_.erase(another_key); + expiry_.erase(another_key); + str_to_int_.erase(int_to_str_[another_key]); + int_to_str_.erase(another_key); + queue_.erase(queue_.begin()); + } + bool created = false; + std::size_t int_key; + if (key.empty() || str_to_int_.count(key) == 0) { + do { + key = utils::generate_random_string(32); + } while (str_to_int_.count(key) != 0); + do { + int_key = dist_(rng_); + } while (int_to_str_.count(int_key) != 0); + str_to_int_[key] = int_key; + int_to_str_[int_key] = key; + sessions_[int_key] = std::make_shared(); + created = true; + } + else { + int_key = str_to_int_[key]; + queue_.erase( + queue_.lower_bound( + std::make_pair(expiry_[int_key], int_key))); + } + // the expiry is set to be 20 minutes from now. + // if the session is re-visited within 20 minutes, + // the expiry will be extended. + expiry_[int_key] = now + std::chrono::minutes(20); + // pushes expiry-key tuple (pair) to the queue + queue_.emplace(expiry_[int_key], int_key); + session_ptr = sessions_[int_key]; + return created; + } + +} // bserv \ No newline at end of file diff --git a/WebApp/bserv/utils.cpp b/WebApp/bserv/utils.cpp new file mode 100644 index 0000000..68ffad1 --- /dev/null +++ b/WebApp/bserv/utils.cpp @@ -0,0 +1,236 @@ +#include "pch.h" +#include "bserv/utils.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace bserv::utils { + + namespace internal { + + // NOTE: + // - `random_device` is implementation dependent. + // it doesn't work with GNU GCC on Windows. + // - for thread-safety, do not directly use it. + // use `get_rd_value` instead. + std::random_device rd; + std::mutex rd_mutex; + + std::random_device::result_type get_rd_value() { + std::lock_guard lg{ rd_mutex }; + return rd(); + } + + // const std::string chars = "abcdefghijklmnopqrstuvwxyz" + // "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + // "1234567890" + // "!@#$%^&*()" + // "`~-_=+[{]}\\|;:'\",<.>/? "; + + const std::string chars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "1234567890"; + + + const std::string url_safe_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789-._~"; + + } // internal + + // https://www.boost.org/doc/libs/1_75_0/libs/random/example/password.cpp + std::string generate_random_string(std::size_t len) { + std::string s; + std::mt19937 rng{ internal::get_rd_value() }; + std::uniform_int_distribution<> dist{ 0, (int)internal::chars.length() - 1 }; + for (std::size_t i = 0; i < len; ++i) s += internal::chars[dist(rng)]; + return s; + } + + namespace security { + + // https://codahale.com/a-lesson-in-timing-attacks/ + bool constant_time_compare(const std::string& a, const std::string& b) { + if (a.length() != b.length()) + return false; + int result = 0; + for (std::size_t i = 0; i < a.length(); ++i) + result |= a[i] ^ b[i]; + return result == 0; + } + + // https://cryptopp.com/wiki/PKCS5_PBKDF2_HMAC + std::string hash_password( + const std::string& password, + const std::string& salt, + unsigned int iterations) { + using namespace CryptoPP; + byte derived[SHA256::DIGESTSIZE]; + PKCS5_PBKDF2_HMAC pbkdf; + byte unused = 0; + pbkdf.DeriveKey(derived, sizeof(derived), unused, + (const byte*)password.c_str(), password.length(), + (const byte*)salt.c_str(), salt.length(), + iterations, 0.0f); + std::string result; + Base64Encoder encoder{ new StringSink{result}, false }; + encoder.Put(derived, sizeof(derived)); + encoder.MessageEnd(); + return result; + } + + std::string encode_password(const std::string& password) { + std::string salt = generate_random_string(16); + std::string hashed_password = hash_password(password, salt); + return salt + '$' + hashed_password; + } + + bool check_password(const std::string& password, + const std::string& encoded_password) { + std::string salt, hashed_password; + std::string* a = &salt, * b = &hashed_password; + for (std::size_t i = 0; i < encoded_password.length(); ++i) { + if (encoded_password[i] != '$') { + (*a) += encoded_password[i]; + } + else { + std::swap(a, b); + } + } + return constant_time_compare( + hash_password(password, salt), hashed_password); + } + + } // security + + + // reference for url: + // https://www.ietf.org/rfc/rfc3986.txt + + // reserved = gen-delims / sub-delims + // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + // / "*" / "+" / "," / ";" / "=" + + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + + // https://stackoverflow.com/questions/54060359/encoding-decoded-urls-in-c + // there can be exceptions (std::stoi)! + std::string decode_url(const std::string& s) { + std::string r; + for (std::size_t i = 0; i < s.length(); ++i) { + if (s[i] == '%') { + int v = std::stoi(s.substr(i + 1, 2), nullptr, 16); + r.push_back(0xff & v); + i += 2; + } + else if (s[i] == '+') r.push_back(' '); + else r.push_back(s[i]); + } + return r; + } + + std::string encode_url(const std::string& s) { + std::ostringstream oss; + for (auto& c : s) { + if (internal::url_safe_characters.find(c) != std::string::npos) { + oss << c; + } + else { + oss << '%' << std::setfill('0') << std::setw(2) << + std::uppercase << std::hex << (0xff & c); + } + } + return oss.str(); + } + + std::pair< + std::map, + std::map>> + parse_params(std::string& s, std::size_t start_pos, char delimiter) { + std::map dict_params; + std::map> list_params; + // we use the swap pointer technique + // we will always append characters to *a only. + std::string key, value, * a = &key, * b = &value; + // append an extra `delimiter` so that the last key-value pair + // is processed just like the other. + s.push_back(delimiter); + for (std::size_t i = start_pos; i < s.length(); ++i) { + if (s[i] == '=') { + std::swap(a, b); + } + else if (s[i] == delimiter) { + // swap(a, b); + a = &key; + b = &value; + // prevent ending with ' ' + while (!key.empty() && key.back() == ' ') key.pop_back(); + while (!value.empty() && value.back() == ' ') value.pop_back(); + if (key.empty() && value.empty()) + continue; + key = decode_url(key); + value = decode_url(value); + // if `key` is in `list_params`, append `value`. + if (list_params.find(key) != list_params.end()) { + list_params[key].push_back(value); + } + else { // `key` is not in `list_params` + auto p = dict_params.find(key); + // if `key` is in `dict_params`, + // move previous value and `value` to `list_params` + // and remove `key` in `dict_params`. + if (p != dict_params.end()) { + list_params[key] = { p->second, value }; + dict_params.erase(p); + } + else { // `key` is not in `dict_params` + dict_params[key] = value; + } + } + // clear `key` and `value` + key = ""; + value = ""; + } + else { + // prevent beginning with ' ' + if (a->empty() && s[i] == ' ') { + continue; + } + (*a) += s[i]; + } + } + // remove the last `delimiter` to restore `s` to what it was. + s.pop_back(); + return std::make_pair(dict_params, list_params); + } + + std::tuple, + std::map>> + parse_url(std::string& s) { + std::string url; + std::size_t i = 0; + for (; i < s.length(); ++i) { + if (s[i] != '?') { + url += s[i]; + } + else { + break; + } + } + if (i == s.length()) + return std::make_tuple(url, + std::map{}, + std::map>{}); + auto&& [dict_params, list_params] = parse_params(s, i + 1); + return std::make_tuple(url, dict_params, list_params); + } + +} // bserv::utils \ No newline at end of file diff --git a/bserv/CMakeLists.txt b/bserv/CMakeLists.txt deleted file mode 100644 index df97293..0000000 --- a/bserv/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.10) - -project(bserv) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED True) - -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -set(CMAKE_CXX_FLAGS "-Wall -Wextra") -set(CMAKE_CXX_FLAGS_DEBUG "-g") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") - -add_library(bserv server.cpp) -target_link_libraries(bserv - pthread - boost_thread - boost_coroutine - boost_log - boost_log_setup - boost_json - pqxx - pq - cryptopp) diff --git a/bserv/client.hpp b/bserv/client.hpp deleted file mode 100644 index f9f88c9..0000000 --- a/bserv/client.hpp +++ /dev/null @@ -1,204 +0,0 @@ -#ifndef _CLIENT_HPP -#define _CLIENT_HPP - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "logging.hpp" - -namespace bserv { - -namespace beast = boost::beast; -namespace http = beast::http; -namespace asio = boost::asio; -namespace json = boost::json; -using asio::ip::tcp; - -using request_type = http::request; -using response_type = http::response; - -class request_failed_exception - : public std::exception { -private: - const std::string msg_; -public: - request_failed_exception(const std::string& msg) : msg_{msg} {} - const char* what() const noexcept { return msg_.c_str(); } -}; - -// https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/client/async/http_client_async.cpp -// https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/client/coro/http_client_coro.cpp - -// sends one async request to a remote server -inline http::response http_client_send( - asio::io_context& ioc, - asio::yield_context& yield, - const std::string& host, - const std::string& port, - const http::request& req) { - beast::error_code ec; - tcp::resolver resolver{ioc}; - const auto results = resolver.async_resolve(host, port, yield[ec]); - if (ec) { - throw request_failed_exception{"http_client_session::resolver resolve: " + ec.message()}; - } - beast::tcp_stream stream{ioc}; - // sets a timeout on the operation - stream.expires_after(std::chrono::seconds(EXPIRY_TIME)); - // makes the connection on the IP address we get from a lookup - stream.async_connect(results, yield[ec]); - if (ec) { - throw request_failed_exception{"http_client_session::stream connect: " + ec.message()}; - } - // sets a timeout on the operation - stream.expires_after(std::chrono::seconds(EXPIRY_TIME)); - // sends the HTTP request to the remote host - http::async_write(stream, req, yield[ec]); - if (ec) { - throw request_failed_exception{"http_client_session::stream write: " + ec.message()}; - } - beast::flat_buffer buffer; - http::response res; - // receives the HTTP response - http::async_read(stream, buffer, res, yield[ec]); - if (ec) { - throw request_failed_exception{"http_client_session::stream read: " + ec.message()}; - } - // gracefully close the socket - stream.socket().shutdown(tcp::socket::shutdown_both, ec); - // `not_connected` happens sometimes so don't bother reporting it - if (ec && ec != beast::errc::not_connected) { - // reports the error to the log! - fail(ec, "http_client_session::stream::socket shutdown"); - // return; - } - // if we get here then the connection is closed gracefully - return res; -} - -inline request_type get_request( - const std::string& host, - const std::string& target, - const http::verb& method, - const boost::json::value& val) { - request_type req; - req.method(method); - req.target(target); - req.set(http::field::host, host); - req.set(http::field::user_agent, NAME); - req.set(http::field::content_type, "application/json"); - req.body() = boost::json::serialize(val); - req.prepare_payload(); - return req; -} - -class http_client { -private: - asio::io_context& ioc_; - asio::yield_context& yield_; -public: - http_client(asio::io_context& ioc, asio::yield_context& yield) - : ioc_{ioc}, yield_{yield} {} - http::response request( - const std::string& host, - const std::string& port, - const http::request& req) { - return http_client_send(ioc_, yield_, host, port, req); - } - boost::json::value request_for_value( - const std::string& host, - const std::string& port, - const http::request& req) { - return boost::json::parse(request(host, port, req).body()); - } - - response_type send( - const std::string& host, - const std::string& port, - const std::string& target, - const http::verb& method, - const boost::json::value& val) { - request_type req = get_request(host, target, method, val); - return request(host, port, req); - } - boost::json::value send_for_value( - const std::string& host, - const std::string& port, - const std::string& target, - const http::verb& method, - const boost::json::value& val) { - request_type req = get_request(host, target, method, val); - return request_for_value(host, port, req); - } - - response_type get( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send(host, port, target, http::verb::get, val); - } - boost::json::value get_for_value( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send_for_value(host, port, target, http::verb::get, val); - } - response_type put( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send(host, port, target, http::verb::put, val); - } - boost::json::value put_for_value( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send_for_value(host, port, target, http::verb::put, val); - } - response_type post( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send(host, port, target, http::verb::post, val); - } - boost::json::value post_for_value( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send_for_value(host, port, target, http::verb::post, val); - } - response_type delete_( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send(host, port, target, http::verb::delete_, val); - } - boost::json::value delete_for_value( - const std::string& host, - const std::string& port, - const std::string& target, - const boost::json::value& val) { - return send_for_value(host, port, target, http::verb::delete_, val); - } -}; - -} // bserv - -#endif // _CLIENT_HPP \ No newline at end of file diff --git a/bserv/config.hpp b/bserv/config.hpp deleted file mode 100644 index 476417a..0000000 --- a/bserv/config.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef _CONFIG_HPP -#define _CONFIG_HPP - -#include -#include -#include -#include -#include - -namespace bserv { - -const std::string NAME = "bserv"; - -const unsigned short PORT = 8080; -const int NUM_THREADS = - std::thread::hardware_concurrency() > 0 ? std::thread::hardware_concurrency() : 1; - -const std::size_t PAYLOAD_LIMIT = 8 * 1024 * 1024; -const int EXPIRY_TIME = 30; // seconds - -const std::size_t LOG_ROTATION_SIZE = 8 * 1024 * 1024; -const std::string LOG_PATH = "./log/" + NAME; - -const int NUM_DB_CONN = 10; -const std::string DB_CONN_STR = "dbname=bserv"; - -#define decl_field(type, name, default_value) \ -private: \ - std::optional name##_; \ -public: \ - void set_##name(std::optional&& name) { name##_ = std::move(name); } \ - type get_##name() const { return name##_.has_value() ? name##_.value() : default_value; } - -struct server_config { - decl_field(std::string, name, NAME) - decl_field(unsigned short, port, PORT) - decl_field(int, num_threads, NUM_THREADS) - decl_field(std::size_t, log_rotation_size, LOG_ROTATION_SIZE) - decl_field(std::string, log_path, LOG_PATH) - decl_field(int, num_db_conn, NUM_DB_CONN) - decl_field(std::string, db_conn_str, DB_CONN_STR) -public: - server_config() = default; -}; - -#undef decl_field - -} // bserv - -#endif // _CONFIG_HPP \ No newline at end of file diff --git a/bserv/database.hpp b/bserv/database.hpp deleted file mode 100644 index ab86283..0000000 --- a/bserv/database.hpp +++ /dev/null @@ -1,380 +0,0 @@ -#ifndef _DATABASE_HPP -#define _DATABASE_HPP - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace bserv { - -using raw_db_connection_type = pqxx::connection; -using raw_db_transaction_type = pqxx::work; - -class db_field { -private: - pqxx::field field_; -public: - db_field(const pqxx::field& field) : field_{field} {} - const char* c_str() const { return field_.c_str(); } - template - Type as() const { return field_.as(); } -}; - -class db_row { -private: - pqxx::row row_; -public: - db_row(const pqxx::row& row) : row_{row} {} - std::size_t size() const { return row_.size(); } - db_field operator[](std::size_t idx) const { return row_[idx]; } -}; - -class db_result { -private: - pqxx::result result_; -public: - class const_iterator { - private: - pqxx::result::const_iterator iterator_; - public: - const_iterator( - const pqxx::result::const_iterator& iterator - ) : iterator_{iterator} {} - const_iterator& operator++() { ++iterator_; return *this; } - bool operator==(const const_iterator& rhs) const { return iterator_ == rhs.iterator_; } - bool operator!=(const const_iterator& rhs) const { return iterator_ != rhs.iterator_; } - db_row operator*() const { return *iterator_; } - }; - db_result() = default; - db_result(const pqxx::result& result) : result_{result} {} - const_iterator begin() const { return result_.begin(); } - const_iterator end() const { return result_.end(); } - std::string query() const { return result_.query(); } -}; - -class db_connection_manager; - -class db_connection { -private: - db_connection_manager& mgr_; - std::shared_ptr conn_; -public: - db_connection( - db_connection_manager& mgr, - std::shared_ptr conn) - : mgr_{mgr}, conn_{conn} {} - // non-copiable, non-assignable - db_connection(const db_connection&) = delete; - db_connection& operator=(const db_connection&) = delete; - // during the destruction, it should put itself back to the - // manager's queue - ~db_connection(); - raw_db_connection_type& get() { return *conn_; } -}; - -// provides the database connection pool functionality -class db_connection_manager { -private: - std::queue> queue_; - // this lock is for manipulating the `queue_` - mutable std::mutex queue_lock_; - // since C++ 17 doesn't provide the semaphore functionality, - // mutex is used to mimic it. (boost provides it) - // if there are no available connections, trying to lock on - // it will cause blocking. - mutable std::mutex counter_lock_; - friend db_connection; -public: - db_connection_manager(const std::string& conn_str, int n) { - for (int i = 0; i < n; ++i) - queue_.emplace( - std::make_shared(conn_str)); - } - // if there are no available database connections, this function - // blocks until there is any; - // otherwise, this function returns a pointer to `db_connection`. - std::shared_ptr get_or_block() { - // `counter_lock_` must be acquired first. - // exchanging this statement with the next will cause dead-lock, - // because if the request is blocked by `counter_lock_`, - // the destructor of `db_connection` will not be able to put - // itself back due to the `queue_lock_` has already been acquired - // by this request! - counter_lock_.lock(); - // `queue_lock_` is acquired so that only one thread will - // modify the `queue_` - std::lock_guard lg{queue_lock_}; - std::shared_ptr conn = queue_.front(); - queue_.pop(); - // if there are no connections in the `queue_`, - // `counter_lock_` remains to be locked - // so that the following requests will be blocked - if (queue_.size() != 0) counter_lock_.unlock(); - return std::make_shared(*this, conn); - } -}; - -inline db_connection::~db_connection() { - std::lock_guard lg{mgr_.queue_lock_}; - mgr_.queue_.emplace(conn_); - // if this is the first available connection back to the queue, - // `counter_lock_` is unlocked so that the blocked requests will - // be notified - if (mgr_.queue_.size() == 1) - mgr_.counter_lock_.unlock(); -} - -// ************************************************************************** - -class db_parameter { -public: - virtual ~db_parameter() = default; - virtual std::string get_value(raw_db_transaction_type&) = 0; -}; - -class db_name : public db_parameter { -private: - std::string value_; -public: - db_name(const std::string& value) - : value_{value} {} - std::string get_value(raw_db_transaction_type& tx) { - return tx.quote_name(value_); - } -}; - -template -class db_value : public db_parameter { -private: - Type value_; -public: - db_value(const Type& value) - : value_{value} {} - std::string get_value(raw_db_transaction_type&) { - return std::to_string(value_); - } -}; - -template <> -class db_value : public db_parameter { -private: - std::string value_; -public: - db_value(const std::string& value) - : value_{value} {} - std::string get_value(raw_db_transaction_type& tx) { - return tx.quote(value_); - } -}; - -template <> -class db_value : public db_parameter { -private: - bool value_; -public: - db_value(const bool& value) - : value_{value} {} - std::string get_value(raw_db_transaction_type&) { - return value_ ? "true" : "false"; - } -}; - -template -class db_value> : public db_parameter { -private: - std::vector value_; -public: - db_value(const std::vector& value) - : value_{value} {} - std::string get_value(raw_db_transaction_type& tx) { - std::string res; - for (const auto& elem : value_) { - if (res.size() != 0) res += ", "; - res += db_value{elem}.get_value(tx); - } - return "ARRAY[" + res + "]"; - } -}; - -namespace db_internal { - -template -std::shared_ptr convert_parameter( - const Param& param) { - return std::make_shared>(param); -} - -template -std::shared_ptr convert_parameter( - const db_value& param) { - return std::make_shared>(param); -} - -inline std::shared_ptr convert_parameter( - const char* param) { - return std::make_shared>(param); -} - -inline std::shared_ptr convert_parameter( - const db_name& param) { - return std::make_shared(param); -} - -template -std::vector convert_parameters( - raw_db_transaction_type& tx, std::shared_ptr... params) { - return {params->get_value(tx)...}; -} - -// ************************************* - -class db_field_holder { -protected: - std::string name_; -public: - db_field_holder(const std::string& name) - : name_{name} {} - virtual ~db_field_holder() = default; - virtual void add( - const db_row& row, std::size_t field_idx, - boost::json::object& obj) = 0; -}; - -template -class db_field : public db_field_holder { -public: - using db_field_holder::db_field_holder; - void add( - const db_row& row, std::size_t field_idx, - boost::json::object& obj) { - obj[name_] = row[field_idx].as(); - } -}; - -template <> -class db_field : public db_field_holder { -public: - using db_field_holder::db_field_holder; - void add( - const db_row& row, std::size_t field_idx, - boost::json::object& obj) { - obj[name_] = row[field_idx].c_str(); - } -}; - -} // db_internal - -template -std::shared_ptr make_db_field( - const std::string& name) { - return std::make_shared>(name); -} - -class invalid_operation_exception : public std::exception { -private: - std::string msg_; -public: - invalid_operation_exception(const std::string& msg) - : msg_{msg} {} - const char* what() const noexcept { return msg_.c_str(); } -}; - -class db_relation_to_object { -private: - std::vector> fields_; -public: - db_relation_to_object( - const std::initializer_list< - std::shared_ptr>& fields) - : fields_{fields} {} - boost::json::object convert_row(const db_row& row) { - boost::json::object obj; - for (std::size_t i = 0; i < fields_.size(); ++i) - fields_[i]->add(row, i, obj); - return obj; - } - std::vector convert_to_vector( - const db_result& result) { - std::vector results; - for (const auto& row : result) - results.emplace_back(convert_row(row)); - return results; - } - std::optional convert_to_optional( - const db_result& result) { - // result.size() == 0 - if (result.begin() == result.end()) return std::nullopt; - auto iterator = result.begin(); - auto first = iterator; - // result.size() == 1 - if (++iterator == result.end()) - return convert_row(*first); - // result.size() > 1 - throw invalid_operation_exception{ - "too many objects to convert"}; - } -}; - -class db_transaction { -private: - raw_db_transaction_type tx_; -public: - db_transaction( - std::shared_ptr connection_ptr - ) : tx_{connection_ptr->get()} {} - // non-copiable, non-assignable - db_transaction(const db_transaction&) = delete; - db_transaction& operator=(const db_transaction&) = delete; - // Usage: - // exec("select * from ? where ? = ? and first_name = 'Name??'", - // db_name("auth_user"), db_name("is_active"), db_value(true)); - // -> SQL: select * from "auth_user" where "is_active" = true and first_name = 'Name?' - // ====================================================================================== - // exec("select * from ? where ? = ? and first_name = ?", - // db_name("auth_user"), db_name("is_active"), false, "Name??"); - // -> SQL: select * from "auth_user" where "is_active" = false and first_name = 'Name??' - // ====================================================================================== - // Note: "?" is the placeholder for parameters, and "??" will be converted to "?" in SQL. - // But, "??" in the parameters remains. - template - db_result exec(const std::string& s, const Params&... params) { - std::vector param_vec = - db_internal::convert_parameters( - tx_, db_internal::convert_parameter(params)...); - std::size_t idx = 0; - std::string query; - for (std::size_t i = 0; i < s.length(); ++i) { - if (s[i] == '?') { - if (i + 1 < s.length() && s[i + 1] == '?') { - query += s[++i]; - } else { - if (idx < param_vec.size()) { - query += param_vec[idx++]; - } else throw std::out_of_range{"too few parameters"}; - } - } else query += s[i]; - } - if (idx != param_vec.size()) - throw invalid_operation_exception{"too many parameters"}; - return tx_.exec(query); - } - void commit() { tx_.commit(); } - void abort() { tx_.abort(); } -}; - - -// TODO: add support for time conversions between postgresql and c++, use timestamp? -// what about time zone? - -} // bserv - -#endif // _DATABASE_HPP \ No newline at end of file diff --git a/bserv/logging.hpp b/bserv/logging.hpp deleted file mode 100644 index 59c6c7d..0000000 --- a/bserv/logging.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef _LOGGING_HPP -#define _LOGGING_HPP - -#define BOOST_LOG_DYN_LINK - -#include -#include -#include -#include - -#include -#include - -#include "config.hpp" - -namespace bserv { - -namespace logging = boost::log; -namespace keywords = boost::log::keywords; -namespace src = boost::log::sources; - -// this function should be called before logging is used -inline void init_logging(const server_config& config) { - logging::add_file_log( - keywords::file_name = config.get_log_path() + "_%Y%m%d_%H-%M-%S.%N.log", - keywords::rotation_size = config.get_log_rotation_size(), - keywords::format = "[%Severity%][%TimeStamp%][%ThreadID%]: %Message%" - ); - logging::core::get()->set_filter( - logging::trivial::severity >= logging::trivial::trace - ); - logging::add_common_attributes(); -} - -#define lgtrace BOOST_LOG_TRIVIAL(trace) -#define lgdebug BOOST_LOG_TRIVIAL(debug) -#define lginfo BOOST_LOG_TRIVIAL(info) -#define lgwarning BOOST_LOG_TRIVIAL(warning) -#define lgerror BOOST_LOG_TRIVIAL(error) -#define lgfatal BOOST_LOG_TRIVIAL(fatal) - -inline void fail(const boost::system::error_code& ec, const char* what) { - lgerror << what << ": " << ec.message() << std::endl; -} - -} // bserv - -#endif // _LOGGING_HPP \ No newline at end of file diff --git a/bserv/router.hpp b/bserv/router.hpp deleted file mode 100644 index e494cb8..0000000 --- a/bserv/router.hpp +++ /dev/null @@ -1,414 +0,0 @@ -#ifndef _ROUTER_HPP -#define _ROUTER_HPP - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "client.hpp" -#include "database.hpp" -#include "session.hpp" -#include "utils.hpp" -#include "config.hpp" -#include "websocket.hpp" - -namespace bserv { - -namespace beast = boost::beast; -namespace http = beast::http; - -struct server_resources { - std::shared_ptr session_mgr; - std::shared_ptr db_conn_mgr; -}; - -struct request_resources { - server_resources& resources; - - asio::io_context& ioc; - asio::yield_context& yield; - std::shared_ptr ws_session; - const std::vector& url_params; - request_type& request; - response_type& response; - - std::shared_ptr session_ptr; - std::shared_ptr db_connection_ptr; - std::shared_ptr http_client_ptr; - std::shared_ptr websocket_server_ptr; -}; - -namespace placeholders { - -template -struct placeholder {}; - -#define make_place_holder(x) constexpr placeholder _##x - -make_place_holder(1); -make_place_holder(2); -make_place_holder(3); -make_place_holder(4); -make_place_holder(5); -make_place_holder(6); -make_place_holder(7); -make_place_holder(8); -make_place_holder(9); - -#undef make_place_holder - -// std::shared_ptr -constexpr placeholder<-1> session; -// bserv::request_type& -constexpr placeholder<-2> request; -// bserv::response_type& -constexpr placeholder<-3> response; -// boost::json::object&& -constexpr placeholder<-4> json_params; -// std::shared_ptr -constexpr placeholder<-5> db_connection_ptr; -// std::shared_ptr -constexpr placeholder<-6> http_client_ptr; -// std::shared_ptr -constexpr placeholder<-7> websocket_server_ptr; - -} // placeholders - -class bad_request_exception : public std::exception { -public: - bad_request_exception() = default; - const char* what() const noexcept { return "bad request"; } -}; - -namespace router_internal { - -template -struct parameter_pack; - -template <> -struct parameter_pack<> {}; - -template -struct parameter_pack - : parameter_pack { - Head head_; - template - parameter_pack(Head2&& head, Tail2&& ...tail) - : parameter_pack{static_cast(tail)...}, - head_{static_cast(head)} {} -}; - -template -struct get_parameter_pack; - -template -struct get_parameter_pack - : get_parameter_pack {}; - -template -struct get_parameter_pack<0, Head, Tail...> { - using type = parameter_pack; -}; - -template -decltype(auto) get_parameter_value(parameter_pack& params) { - return (static_cast< - typename get_parameter_pack::type& - >(params).head_); -} - -template -struct get_parameter; - -template -struct get_parameter - : get_parameter {}; - -template -struct get_parameter<0, Head, Tail...> { - using type = Head; -}; - -template -Type&& get_parameter_data( - request_resources&, Type&& val) { - return static_cast(val); -} - -template = 0), int> = 0> -const std::string& get_parameter_data( - request_resources& resources, - placeholders::placeholder) { - return resources.url_params[N]; -} - -inline std::shared_ptr get_parameter_data( - request_resources& resources, - placeholders::placeholder<-1>) { - if (resources.session_ptr != nullptr) - return resources.session_ptr; - std::string cookie_str{resources.request[http::field::cookie]}; - auto&& [cookie_dict, cookie_list] - = utils::parse_params(cookie_str, 0, ';'); - boost::ignore_unused(cookie_list); - std::string session_id; - if (cookie_dict.count(SESSION_NAME) != 0) { - session_id = cookie_dict[SESSION_NAME]; - } - std::shared_ptr session_ptr; - if (resources.resources.session_mgr->get_or_create(session_id, session_ptr)) { - resources.response.set(http::field::set_cookie, SESSION_NAME + "=" + session_id); - } - resources.session_ptr = session_ptr; - return session_ptr; -} - -inline request_type& get_parameter_data( - request_resources& resources, - placeholders::placeholder<-2>) { - return resources.request; -} - -inline response_type& get_parameter_data( - request_resources& resources, - placeholders::placeholder<-3>) { - return resources.response; -} - -inline boost::json::object get_parameter_data( - request_resources& resources, - placeholders::placeholder<-4>) { - std::string target{resources.request.target()}; - auto&& [url, dict_params, list_params] = utils::parse_url(target); - boost::ignore_unused(url); - boost::json::object body; - if (!resources.request.body().empty()) { - try { - body = boost::json::parse(resources.request.body()).as_object(); - } catch (const std::exception& e) { - throw bad_request_exception{}; - } - } - for (auto& [k, v] : dict_params) { - if (!body.contains(k)) { - body[k] = v; - } - } - for (auto& [k, vs] : list_params) { - if (!body.contains(k)) { - boost::json::array a; - for (auto& v : vs) { - a.push_back(boost::json::string{v}); - } - body[k] = a; - } - } - return body; -} - -inline std::shared_ptr get_parameter_data( - request_resources& resources, - placeholders::placeholder<-5>) { - if (resources.db_connection_ptr == nullptr) - resources.db_connection_ptr = - resources.resources.db_conn_mgr->get_or_block(); - return resources.db_connection_ptr; -} - -inline std::shared_ptr get_parameter_data( - request_resources& resources, - placeholders::placeholder<-6>) { - if (resources.http_client_ptr == nullptr) - resources.http_client_ptr = - std::make_shared(resources.ioc, resources.yield); - return resources.http_client_ptr; -} - -inline std::shared_ptr get_parameter_data( - request_resources& resources, - placeholders::placeholder<-7>) { - if (resources.websocket_server_ptr == nullptr) - resources.websocket_server_ptr = - std::make_shared(*resources.ws_session, resources.yield); - return resources.websocket_server_ptr; -} - -template -struct path_handler; - -template -struct path_handler> { - Ret invoke(request_resources& resources, - Ret (*pf)(Args ...), parameter_pack& params) { - if constexpr (Idx == 0) return (*pf)(); - else return static_cast, - typename get_parameter::type>* - >(this)->invoke2(resources, pf, params, - get_parameter_data(resources, - get_parameter_value(params))); - } -}; - -template -struct path_handler, Head, Tail...> - : path_handler, Tail...> { - template < - typename Head2, typename ...Tail2, - std::enable_if_t = 0> - Ret invoke2(request_resources& resources, - Ret (*pf)(Args ...), parameter_pack& params, - Head2&& head2, Tail2&& ...tail2) { - if constexpr (Idx == 0) - return (*pf)(static_cast(head2), - static_cast(tail2)...); - else return static_cast, - typename get_parameter::type, Head, Tail...>* - >(this)->invoke2(resources, pf, params, - get_parameter_data(resources, - get_parameter_value(params)), - static_cast(head2), static_cast(tail2)...); - } -}; - -const std::vector> url_regex_mapping{ - {std::regex{""}, "([0-9]+)"}, - {std::regex{""}, R"(([A-Za-z0-9_\.\-]+))"}, - {std::regex{""}, R"(([A-Za-z0-9_/\.\-]+))"} -}; - -inline std::string get_re_url(const std::string& url) { - std::string re_url = url; - for (auto& [r, s] : url_regex_mapping) - re_url = std::regex_replace(re_url, r, s); - return re_url; -} - -struct path_holder : std::enable_shared_from_this { - path_holder() = default; - virtual ~path_holder() = default; - virtual bool match( - const std::string&, - std::vector&) const = 0; - virtual std::optional invoke( - request_resources&) = 0; -}; - -template -class path; - -template -class path> - : public path_holder { -private: - std::regex re_; - Ret (*pf_)(Args ...); - parameter_pack params_; - path_handler<0, Ret (*)(Args ...), parameter_pack, Params...> handler_; -public: - path(const std::string& url, Ret (*pf)(Args ...), Params&& ...params) - : re_{get_re_url(url)}, pf_{pf}, - params_{static_cast(params)...} {} - bool match(const std::string& url, std::vector& result) const { - std::smatch r; - bool matched = std::regex_match(url, r, re_); - if (matched) { - result.clear(); - for (auto & sub : r) - result.push_back(sub.str()); - } - return matched; - } - std::optional invoke( - request_resources& resources) { - return handler_.invoke( - resources, pf_, params_); - } -}; - -} // router_internal - -template -std::shared_ptr>> make_path( - const std::string& url, Ret (*pf)(Args ...), Params&& ...params) { - return std::make_shared< - router_internal::path> - >(url, pf, static_cast(params)...); -} - -template -std::shared_ptr>> make_path( - const char* url, Ret (*pf)(Args ...), Params&& ...params) { - return std::make_shared< - router_internal::path> - >(url, pf, static_cast(params)...); -} - -class url_not_found_exception : public std::exception { -public: - url_not_found_exception() = default; - const char* what() const noexcept { return "url not found"; } -}; - -class router { -private: - using path_holder_type = std::shared_ptr; - std::vector paths_; - std::shared_ptr resources_; -public: - router(const std::initializer_list& paths) - : paths_{paths} {} - void set_resources(std::shared_ptr resources) { - resources_ = resources; - } - std::optional operator()( - asio::io_context& ioc, asio::yield_context& yield, - std::shared_ptr ws_session, - const std::string& url, request_type& request, response_type& response) { - std::vector url_params; - for (auto& ptr : paths_) { - if (ptr->match(url, url_params)) { - request_resources resources{ - *resources_, - - ioc, - yield, - ws_session, - url_params, - request, - response, - - nullptr, - nullptr, - nullptr, - nullptr - }; - return ptr->invoke(resources); - } - } - throw url_not_found_exception{}; - } -}; - -} // bserv - -#endif // _ROUTER_HPP \ No newline at end of file diff --git a/bserv/server.cpp b/bserv/server.cpp deleted file mode 100644 index 834eebb..0000000 --- a/bserv/server.cpp +++ /dev/null @@ -1,503 +0,0 @@ -#include "server.hpp" - -#include "logging.hpp" -#include "utils.hpp" -#include "client.hpp" -#include "websocket.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bserv { - - -std::string get_address(const tcp::socket& socket) { - tcp::endpoint end_point = socket.remote_endpoint(); - std::string addr = end_point.address().to_string() - + ':' + std::to_string(end_point.port()); - return addr; -} - -http::response handle_request( - http::request& req, router& routes, - std::shared_ptr ws_session, - asio::io_context& ioc, asio::yield_context& yield) { - - const auto bad_request = [&req](beast::string_view why) { - http::response res{ - http::status::bad_request, req.version()}; - res.set(http::field::server, NAME); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = std::string{why}; - res.prepare_payload(); - return res; - }; - - const auto not_found = [&req](beast::string_view target) { - http::response res{ - http::status::not_found, req.version()}; - res.set(http::field::server, NAME); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "The requested url '" - + std::string{target} + "' does not exist."; - res.prepare_payload(); - return res; - }; - - const auto server_error = [&req](beast::string_view what) { - http::response res{ - http::status::internal_server_error, req.version()}; - res.set(http::field::server, NAME); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "Internal server error: " + std::string{what}; - res.prepare_payload(); - return res; - }; - - boost::string_view target = req.target(); - auto pos = target.find('?'); - boost::string_view url; - if (pos == boost::string_view::npos) url = target; - else url = target.substr(0, pos); - - http::response res{ - http::status::ok, req.version()}; - res.set(http::field::server, NAME); - res.set(http::field::content_type, "application/json"); - res.keep_alive(req.keep_alive()); - - std::optional val; - try { - val = routes(ioc, yield, ws_session, std::string{url}, req, res); - } catch (const url_not_found_exception& e) { - return not_found(url); - } catch (const bad_request_exception& e) { - return bad_request("Request body is not a valid JSON string."); - } catch (const std::exception& e) { - return server_error(e.what()); - } catch (...) { - return server_error("Unknown exception."); - } - - if (val.has_value()) { - res.body() = json::serialize(val.value()); - res.prepare_payload(); - } - - return res; -} - -class websocket_session_server; - -void handle_websocket_request( - std::shared_ptr, - std::shared_ptr session, - http::request& req, router& routes, - asio::io_context& ioc, asio::yield_context yield); - -class websocket_session_server - : public std::enable_shared_from_this { -private: - friend websocket_server; - std::string address_; - std::shared_ptr session_; - http::request req_; - router& routes_; - void on_accept(beast::error_code ec) { - if (ec) { - fail(ec, "websocket_session_server accept"); - return; - } - // handles request here - asio::spawn( - session_->ioc_, - std::bind( - &handle_websocket_request, - shared_from_this(), - session_, - std::ref(req_), - std::ref(routes_), - std::ref(session_->ioc_), - std::placeholders::_1)); - } -public: - explicit websocket_session_server( - asio::io_context& ioc, - tcp::socket&& socket, - http::request&& req, - router& routes) - : address_{get_address(socket)}, - session_{std::make_shared< - websocket_session>(address_, ioc, std::move(socket))}, - req_{std::move(req)}, routes_{routes} { - lgtrace << "websocket_session_server opened: " << address_; - } - ~websocket_session_server() { - lgtrace << "websocket_session_server closed: " << address_; - } - // starts the asynchronous accept operation - void do_accept() { - // sets suggested timeout settings for the websocket - session_->ws_.set_option( - websocket::stream_base::timeout::suggested( - beast::role_type::server)); - // sets a decorator to change the Server of the handshake - session_->ws_.set_option( - websocket::stream_base::decorator( - [](websocket::response_type& res) { - res.set( - http::field::server, - std::string{BOOST_BEAST_VERSION_STRING} + " websocket-server"); - })); - // accepts the websocket handshake - session_->ws_.async_accept( - req_, - beast::bind_front_handler( - &websocket_session_server::on_accept, - shared_from_this())); - } -}; - -void handle_websocket_request( - std::shared_ptr, - std::shared_ptr session, - http::request& req, router& routes, - asio::io_context& ioc, asio::yield_context yield) { - handle_request(req, routes, session, ioc, yield); -} - -std::string websocket_server::read() { - beast::error_code ec; - beast::flat_buffer buffer; - // reads a message into the buffer - session_.ws_.async_read(buffer, yield_[ec]); - lgtrace << "websocket_server: read from " << session_.address_; - // this indicates that the session was closed - if (ec == websocket::error::closed) { - throw websocket_closed{}; - } - if (ec) { - fail(ec, "websocket_server read"); - throw websocket_io_exception{"websocket_server read: " + ec.message()}; - } - // lgtrace << "websocket_server: received text? " << ws_.got_text() << " from " << address_; - return beast::buffers_to_string(buffer.data()); -} - -void websocket_server::write(const std::string& data) { - beast::error_code ec; - // ws_.text(ws_.got_text()); - session_.ws_.async_write(asio::buffer(data), yield_[ec]); - lgtrace << "websocket_server: write to " << session_.address_; - if (ec) { - fail(ec, "websocket_server write"); - throw websocket_io_exception{"websocket_server write: " + ec.message()}; - } -} - - -class http_session; - -// this function produces an HTTP response for the given -// request. The type of the response object depends on the -// contents of the request, so the interface requires the -// caller to pass a generic lambda for receiving the response. -// NOTE: `send` should be called only once! -template -void handle_http_request( - std::shared_ptr, - http::request req, - Send& send, router& routes, asio::io_context& ioc, asio::yield_context yield) { - send(handle_request(req, routes, nullptr, ioc, yield)); -} - -// handles an HTTP server connection -class http_session - : public std::enable_shared_from_this { -private: - // the function object is used to send an HTTP message. - class send_lambda { - private: - http_session& self_; - public: - send_lambda(http_session& self) - : self_{self} {} - template - void operator()( - http::message&& msg) const { - // the lifetime of the message has to extend - // for the duration of the async operation so - // we use a shared_ptr to manage it. - auto sp = std::make_shared< - http::message>( - std::move(msg)); - // stores a type-erased version of the shared - // pointer in the class to keep it alive. - self_.res_ = sp; - // writes the response - http::async_write( - self_.stream_, *sp, - beast::bind_front_handler( - &http_session::on_write, - self_.shared_from_this(), - sp->need_eof())); - } - } lambda_; - asio::io_context& ioc_; - beast::tcp_stream stream_; - beast::flat_buffer buffer_; - boost::optional< - http::request_parser> parser_; - std::shared_ptr res_; - router& routes_; - router& ws_routes_; - const std::string address_; - void do_read() { - // constructs a new parser for each message - parser_.emplace(); - // applies a reasonable limit to the allowed size - // of the body in bytes to prevent abuse. - parser_->body_limit(PAYLOAD_LIMIT); - // sets the timeout. - stream_.expires_after(std::chrono::seconds(EXPIRY_TIME)); - // reads a request using the parser-oriented interface - http::async_read( - stream_, buffer_, *parser_, - beast::bind_front_handler( - &http_session::on_read, - shared_from_this())); - } - void on_read( - beast::error_code ec, - std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - lgtrace << "received " << bytes_transferred << " byte(s) from: " << address_; - // this means they closed the connection - if (ec == http::error::end_of_stream) { - do_close(); - return; - } - if (ec) { - fail(ec, "http_session async_read"); - return; - } - - // sees if it is a websocket upgrade - if (websocket::is_upgrade(parser_->get())) { - // creates a websocket session, transferring ownership - // of both the socket and the http request - std::make_shared( - ioc_, - stream_.release_socket(), - parser_->release(), - ws_routes_ - )->do_accept(); - return; - } - - // handles the request and sends the response - - asio::spawn( - ioc_, - std::bind( - &handle_http_request, - shared_from_this(), - parser_->release(), - std::ref(lambda_), - std::ref(routes_), - std::ref(ioc_), - std::placeholders::_1)); - // handle_request(parser_->release(), lambda_, routes_); - - // at this point the parser can be reset - } - void on_write( - bool close, beast::error_code ec, - std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - // we're done with the response so delete it - res_.reset(); - if (ec) { - fail(ec, "http_session async_write"); - return; - } - lgtrace << "sent " << bytes_transferred << " byte(s) to: " << address_; - if (close) { - // this means we should close the connection, usually because - // the response indicated the "Connection: close" semantic. - do_close(); - return; - } - // reads another request - do_read(); - } - void do_close() { - // sends a TCP shutdown - beast::error_code ec; - stream_.socket().shutdown(tcp::socket::shutdown_send, ec); - // at this point the connection is closed gracefully - lgtrace << "socket connection closed: " << address_; - } -public: - http_session( - asio::io_context& ioc, - tcp::socket&& socket, - router& routes, - router& ws_routes) - : lambda_{*this}, - ioc_{ioc}, - stream_{std::move(socket)}, - routes_{routes}, - ws_routes_{ws_routes}, - address_{get_address(stream_.socket())} { - lgtrace << "http session opened: " << address_; - } - ~http_session() { - lgtrace << "http session closed: " << address_; - } - void run() { - asio::dispatch( - stream_.get_executor(), - beast::bind_front_handler( - &http_session::do_read, - shared_from_this())); - } -}; - -// accepts incoming connections and launches the sessions -class listener - : public std::enable_shared_from_this { -private: - asio::io_context& ioc_; - tcp::acceptor acceptor_; - router& routes_; - router& ws_routes_; - void do_accept() { - acceptor_.async_accept( - asio::make_strand(ioc_), - beast::bind_front_handler( - &listener::on_accept, - shared_from_this())); - } - void on_accept(beast::error_code ec, tcp::socket socket) { - if (ec) { - fail(ec, "listener::acceptor async_accept"); - } else { - lgtrace << "listener accepts: " << get_address(socket); - std::make_shared( - ioc_, std::move(socket), routes_, ws_routes_)->run(); - } - do_accept(); - } -public: - listener( - asio::io_context& ioc, - tcp::endpoint endpoint, - router& routes, - router& ws_routes) - : ioc_{ioc}, - acceptor_{asio::make_strand(ioc)}, - routes_{routes}, - ws_routes_{ws_routes} { - beast::error_code ec; - acceptor_.open(endpoint.protocol(), ec); - if (ec) { - fail(ec, "listener::acceptor open"); - exit(EXIT_FAILURE); - return; - } - acceptor_.set_option( - asio::socket_base::reuse_address(true), ec); - if (ec) { - fail(ec, "listener::acceptor set_option"); - exit(EXIT_FAILURE); - return; - } - acceptor_.bind(endpoint, ec); - if (ec) { - fail(ec, "listener::acceptor bind"); - exit(EXIT_FAILURE); - return; - } - acceptor_.listen( - asio::socket_base::max_listen_connections, ec); - if (ec) { - fail(ec, "listener::acceptor listen"); - exit(EXIT_FAILURE); - return; - } - } - void run() { - asio::dispatch( - acceptor_.get_executor(), - beast::bind_front_handler( - &listener::do_accept, - shared_from_this())); - } -}; - - -server::server(const server_config& config, router&& routes, router&& ws_routes) - : ioc_{config.get_num_threads()}, - routes_{std::move(routes)}, - ws_routes_{std::move(ws_routes)} { - init_logging(config); - - // database connection - try { - db_conn_mgr_ = std::make_shared< - db_connection_manager>(config.get_db_conn_str(), config.get_num_db_conn()); - } catch (const std::exception& e) { - lgfatal << "db connection initialization failed: " << e.what() << std::endl; - exit(EXIT_FAILURE); - } - session_mgr_ = std::make_shared(); - - std::shared_ptr resources_ptr = std::make_shared(); - resources_ptr->session_mgr = session_mgr_; - resources_ptr->db_conn_mgr = db_conn_mgr_; - - routes_.set_resources(resources_ptr); - ws_routes_.set_resources(resources_ptr); - - // creates and launches a listening port - std::make_shared( - ioc_, tcp::endpoint{tcp::v4(), config.get_port()}, routes_, ws_routes_)->run(); - - // captures SIGINT and SIGTERM to perform a clean shutdown - asio::signal_set signals{ioc_, SIGINT, SIGTERM}; - signals.async_wait( - [&](const boost::system::error_code&, int) { - // stops the `io_context`. This will cause `run()` - // to return immediately, eventually destroying the - // `io_context` and all of the sockets in it. - ioc_.stop(); - }); - - lginfo << config.get_name() << " started"; - - // runs the I/O service on the requested number of threads - std::vector v; - v.reserve(config.get_num_threads() - 1); - for (int i = 1; i < config.get_num_threads(); ++i) - v.emplace_back([&]{ ioc_.run(); }); - ioc_.run(); - - // if we get here, it means we got a SIGINT or SIGTERM - lginfo << "exiting " << config.get_name(); - - // blocks until all the threads exit - for (auto & t : v) t.join(); -} - -} // bserv \ No newline at end of file diff --git a/bserv/session.hpp b/bserv/session.hpp deleted file mode 100644 index cad8d4b..0000000 --- a/bserv/session.hpp +++ /dev/null @@ -1,108 +0,0 @@ -#ifndef _SESSION_HPP -#define _SESSION_HPP - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "utils.hpp" - -namespace bserv { - -const std::string SESSION_NAME = "bsessionid"; - -// using session_type = std::map; -using session_type = boost::json::object; - -struct session_manager_base - : std::enable_shared_from_this { - virtual ~session_manager_base() = default; - // if `key` refers to an existing session, that session will be placed in - // `session_ptr` and this function will return `false`. - // otherwise, this function will create a new session, place the created - // session in `session_ptr`, place the session id in `key`, and return `true`. - // this means, the returned value indicates whether a new session is created, - // the `session_ptr` will point to a session with `key` as its session id, - // after this function is called. - // NOTE: a `shared_ptr` is returned instead of a reference. - virtual bool get_or_create( - std::string& key, - std::shared_ptr& session_ptr) = 0; -}; - -class memory_session_manager : public session_manager_base { -private: - using time_point = std::chrono::steady_clock::time_point; - std::mt19937 rng_; - std::uniform_int_distribution dist_; - std::map str_to_int_; - std::map int_to_str_; - std::map> sessions_; - // `expiry` stores tuple sorted by key - std::map expiry_; - // `queue` functions as a priority queue - // (the front element is the smallest) - // and stores tuple sorted by - // expiry first and then key. - std::set> queue_; - mutable std::mutex lock_; -public: - memory_session_manager() - : rng_{utils::internal::get_rd_value()}, - dist_{0, std::numeric_limits::max()} {} - bool get_or_create( - std::string& key, - std::shared_ptr& session_ptr) { - std::lock_guard lg{lock_}; - time_point now = std::chrono::steady_clock::now(); - // removes the expired sessions - while (!queue_.empty() && queue_.begin()->first < now) { - std::size_t another_key = queue_.begin()->second; - sessions_.erase(another_key); - expiry_.erase(another_key); - str_to_int_.erase(int_to_str_[another_key]); - int_to_str_.erase(another_key); - queue_.erase(queue_.begin()); - } - bool created = false; - std::size_t int_key; - if (key.empty() || str_to_int_.count(key) == 0) { - do { - key = utils::generate_random_string(32); - } while (str_to_int_.count(key) != 0); - do { - int_key = dist_(rng_); - } while (int_to_str_.count(int_key) != 0); - str_to_int_[key] = int_key; - int_to_str_[int_key] = key; - sessions_[int_key] = std::make_shared(); - created = true; - } else { - int_key = str_to_int_[key]; - queue_.erase( - queue_.lower_bound( - std::make_pair(expiry_[int_key], int_key))); - } - // the expiry is set to be 20 minutes from now. - // if the session is re-visited within 20 minutes, - // the expiry will be extended. - expiry_[int_key] = now + std::chrono::minutes(20); - // pushes expiry-key tuple (pair) to the queue - queue_.emplace(expiry_[int_key], int_key); - session_ptr = sessions_[int_key]; - return created; - } -}; - -} // bserv - -#endif // _SESSION_HPP \ No newline at end of file diff --git a/bserv/utils.hpp b/bserv/utils.hpp deleted file mode 100644 index b4ffa6c..0000000 --- a/bserv/utils.hpp +++ /dev/null @@ -1,246 +0,0 @@ -#ifndef _UTILS_HPP -#define _UTILS_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace bserv::utils { - -namespace internal { - -// NOTE: -// - `random_device` is implementation dependent. -// it doesn't work with GNU GCC on Windows. -// - for thread-safety, do not directly use it. -// use `get_rd_value` instead. -inline std::random_device rd; -inline std::mutex rd_mutex; - -inline auto get_rd_value() { - std::lock_guard lg{rd_mutex}; - return rd(); -} - -// const std::string chars = "abcdefghijklmnopqrstuvwxyz" -// "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -// "1234567890" -// "!@#$%^&*()" -// "`~-_=+[{]}\\|;:'\",<.>/? "; - -const std::string chars = "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "1234567890"; - - -const std::string url_safe_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789-._~"; - -} // internal - -// https://www.boost.org/doc/libs/1_75_0/libs/random/example/password.cpp -inline std::string generate_random_string(std::size_t len) { - std::string s; - std::mt19937 rng{internal::get_rd_value()}; - std::uniform_int_distribution<> dist{0, (int) internal::chars.length() - 1}; - for (std::size_t i = 0; i < len; ++i) s += internal::chars[dist(rng)]; - return s; -} - -namespace security { - -// https://codahale.com/a-lesson-in-timing-attacks/ -inline bool constant_time_compare(const std::string& a, const std::string& b) { - if (a.length() != b.length()) - return false; - int result = 0; - for (std::size_t i = 0; i < a.length(); ++i) - result |= a[i] ^ b[i]; - return result == 0; -} - -// https://cryptopp.com/wiki/PKCS5_PBKDF2_HMAC -inline std::string hash_password( - const std::string& password, - const std::string& salt, - unsigned int iterations = 20000 /*320000*/) { - using namespace CryptoPP; - byte derived[SHA256::DIGESTSIZE]; - PKCS5_PBKDF2_HMAC pbkdf; - byte unused = 0; - pbkdf.DeriveKey(derived, sizeof(derived), unused, - (const byte*) password.c_str(), password.length(), - (const byte*) salt.c_str(), salt.length(), - iterations, 0.0f); - std::string result; - Base64Encoder encoder{new StringSink{result}, false}; - encoder.Put(derived, sizeof(derived)); - encoder.MessageEnd(); - return result; -} - -inline std::string encode_password(const std::string& password) { - std::string salt = generate_random_string(16); - std::string hashed_password = hash_password(password, salt); - return salt + '$' + hashed_password; -} - -inline bool check_password(const std::string& password, - const std::string& encoded_password) { - std::string salt, hashed_password; - std::string* a = &salt, * b = &hashed_password; - for (std::size_t i = 0; i < encoded_password.length(); ++i) { - if (encoded_password[i] != '$') { - (*a) += encoded_password[i]; - } else { - std::swap(a, b); - } - } - return constant_time_compare( - hash_password(password, salt), hashed_password); -} - -} // security - -// reference for url: -// https://www.ietf.org/rfc/rfc3986.txt - -// reserved = gen-delims / sub-delims -// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" -// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" -// / "*" / "+" / "," / ";" / "=" - -// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - -// https://stackoverflow.com/questions/54060359/encoding-decoded-urls-in-c -// there can be exceptions (std::stoi)! -inline std::string decode_url(const std::string& s) { - std::string r; - for (std::size_t i = 0; i < s.length(); ++i) { - if (s[i] == '%') { - int v = std::stoi(s.substr(i + 1, 2), nullptr, 16); - r.push_back(0xff & v); - i += 2; - } else if (s[i] == '+') r.push_back(' '); - else r.push_back(s[i]); - } - return r; -} - -inline std::string encode_url(const std::string& s) { - std::ostringstream oss; - for (auto& c : s) { - if (internal::url_safe_characters.find(c) != std::string::npos) { - oss << c; - } else { - oss << '%' << std::setfill('0') << std::setw(2) << - std::uppercase << std::hex << (0xff & c); - } - } - return oss.str(); -} - -// this function parses param list in the form of k1=v1&k2=v2..., -// where '&' can be any delimiter. -// ki and vi will be converted if they are percent-encoded, -// which is why the returned values are `string`, not `string_view`. -inline -std::pair< - std::map, - std::map>> -parse_params(std::string& s, std::size_t start_pos = 0, char delimiter = '&') { - std::map dict_params; - std::map> list_params; - // we use the swap pointer technique - // we will always append characters to *a only. - std::string key, value, *a = &key, *b = &value; - // append an extra `delimiter` so that the last key-value pair - // is processed just like the other. - s.push_back(delimiter); - for (std::size_t i = start_pos; i < s.length(); ++i) { - if (s[i] == '=') { - std::swap(a, b); - } else if (s[i] == delimiter) { - // swap(a, b); - a = &key; - b = &value; - // prevent ending with ' ' - while (!key.empty() && key.back() == ' ') key.pop_back(); - while (!value.empty() && value.back() == ' ') value.pop_back(); - if (key.empty() && value.empty()) - continue; - key = decode_url(key); - value = decode_url(value); - // if `key` is in `list_params`, append `value`. - auto p = list_params.find(key); - if (p != list_params.end()) { - list_params[key].push_back(value); - } else { // `key` is not in `list_params` - auto p = dict_params.find(key); - // if `key` is in `dict_params`, - // move previous value and `value` to `list_params` - // and remove `key` in `dict_params`. - if (p != dict_params.end()) { - list_params[key] = {p->second, value}; - dict_params.erase(p); - } else { // `key` is not in `dict_params` - dict_params[key] = value; - } - } - // clear `key` and `value` - key = ""; - value = ""; - } else { - // prevent beginning with ' ' - if (a->empty() && s[i] == ' ') { - continue; - } - (*a) += s[i]; - } - } - // remove the last `delimiter` to restore `s` to what it was. - s.pop_back(); - return std::make_pair(dict_params, list_params); -} - -// this function parses url in the form of [url]?k1=v1&k2=v2... -// this function will convert ki and vi if they are percent-encoded. -// NOTE: don't misuse this function, it's going to modify -// the parameter `s` in place! -inline -std::tuple, - std::map>> -parse_url(std::string& s) { - std::string url; - std::size_t i = 0; - for (; i < s.length(); ++i) { - if (s[i] != '?') { - url += s[i]; - } else { - break; - } - } - if (i == s.length()) - return std::make_tuple(url, - std::map{}, - std::map>{}); - auto&& [dict_params, list_params] = parse_params(s, i + 1); - return std::make_tuple(url, dict_params, list_params); -} - -} // bserv::utils - -#endif // _UTILS_HPP \ No newline at end of file diff --git a/bserv/websocket.hpp b/bserv/websocket.hpp deleted file mode 100644 index d9ca245..0000000 --- a/bserv/websocket.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef _WEBSOCKET_HPP -#define _WEBSOCKET_HPP - -#include -#include -#include - -#include -#include -#include -#include - -namespace bserv { - -namespace beast = boost::beast; -namespace http = beast::http; -namespace websocket = beast::websocket; -namespace asio = boost::asio; -namespace json = boost::json; -using asio::ip::tcp; - -class websocket_closed - : public std::exception { -public: - websocket_closed() {} - const char* what() const noexcept { return "websocket session has been closed"; } -}; - -class websocket_io_exception - : public std::exception { -private: - const std::string msg_; -public: - websocket_io_exception(const std::string& msg) : msg_{msg} {} - const char* what() const noexcept { return msg_.c_str(); } -}; - -struct websocket_session { - const std::string address_; - asio::io_context& ioc_; - websocket::stream ws_; - websocket_session( - const std::string& address, - asio::io_context& ioc, - tcp::socket&& socket) - : address_{address}, - ioc_{ioc}, ws_{std::move(socket)} {} -}; - -class websocket_server { -private: - websocket_session& session_; - asio::yield_context& yield_; -public: - websocket_server(websocket_session& session, asio::yield_context& yield) - : session_{session}, yield_{yield} {} - std::string read(); - boost::json::value read_json() { return boost::json::parse(read()); } - void write(const std::string& data); - void write_json(const boost::json::value& val) { write(boost::json::serialize(val)); } -}; - -} // bserv - -#endif // _WEBSOCKET_HPP \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..9eddd1b --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "port": 8080, + "thread-num": 2, + "conn-num": 4, + "conn-str": "postgresql://username:password@localhost:5432/bserv", + "static_root": "../../templates/statics", + "template_root": "../../templates" +} \ No newline at end of file diff --git a/dependencies/README.md b/dependencies/README.md new file mode 100644 index 0000000..7cf9ea5 --- /dev/null +++ b/dependencies/README.md @@ -0,0 +1,58 @@ +# Dependencies + + +## [Boost](https://www.boost.org/) + +CMD: +``` +git clone --single-branch --branch master --recursive https://github.com/boostorg/boost.git +cd boost +bootstrap +b2 +``` + + +## [Crypto++](https://cryptopp.com/) + +CMD: +``` +git clone https://github.com/weidai11/cryptopp.git +``` + +1. Go to `cryptopp`. +2. Use VS2019 to open `cryptest.sln`. +3. For `Debug` `x64` configuration, open `Properties` of `cryptlib` project. In `C/C++` `Code Generation`, set `Runtime Library` to `Multithreading Debug DLL (/MDd)`. +4. For `Release` `x64` configuration, open `Properties` of `cryptlib` project. In `C/C++` `Code Generation`, set `Runtime Library` to `Multithreading DLL (/MD)`. +5. `Batch Build` `Debug` AND `Release` `x64` of `cryptlib`. + + +# [PostgreSQL 14.0](https://www.postgresql.org/) + +1. Use this [link](https://get.enterprisedb.com/postgresql/postgresql-14.0-1-windows-x64-binaries.zip) to download the binaries. +2. Unzip the zip archive here. It should be named `pgsql` and contains `bin`, `include` and `lib`. + + +# [Libpqxx](https://github.com/jtv/libpqxx) + +CMD: +``` +git clone https://github.com/jtv/libpqxx.git +``` + +1. Go to `libpqxx`. +2. Use `cmake-gui`: + - `Browse Source...` and `Browse Build...` to the root directory of `libpqxx`. + - `Add Entry`: `PostgreSQL_INCLUDE_DIR` (`PATH`) = `../../pgsql/include` + - `Add Entry`: `PostgreSQL_LIBRARY` (`FILEPATH`) = `../../pgsql/lib/libpq` + - `Configure`: Use default settings (`VS2019` `x64`). + - `Generate` +3. Use VS2019 to open `libpqxx.sln`. +4. `Batch Build` `Debug` AND `Release` `x64` of `pqxx`. + + +# [inja](https://github.com/pantor/inja) + +CMD: +``` +git clone https://github.com/pantor/inja.git +``` diff --git a/dependencies/boost b/dependencies/boost new file mode 160000 index 0000000..45d1a0a --- /dev/null +++ b/dependencies/boost @@ -0,0 +1 @@ +Subproject commit 45d1a0a0ebaf0d475f420f73a8b40788192134ad diff --git a/dependencies/cryptopp b/dependencies/cryptopp new file mode 160000 index 0000000..131fdc1 --- /dev/null +++ b/dependencies/cryptopp @@ -0,0 +1 @@ +Subproject commit 131fdc1bdf77352d7adec7e9e383efcd9e8f0075 diff --git a/dependencies/inja b/dependencies/inja new file mode 160000 index 0000000..635e1fb --- /dev/null +++ b/dependencies/inja @@ -0,0 +1 @@ +Subproject commit 635e1fb183485eeb8c9a7d6ba893629cd5e00c89 diff --git a/dependencies/libpqxx b/dependencies/libpqxx new file mode 160000 index 0000000..819940f --- /dev/null +++ b/dependencies/libpqxx @@ -0,0 +1 @@ +Subproject commit 819940f96d4be43ce2b7f7006e457e66047d5047 diff --git a/handlers.hpp b/handlers.hpp deleted file mode 100644 index 5cd5d53..0000000 --- a/handlers.hpp +++ /dev/null @@ -1,267 +0,0 @@ -#ifndef _HANDLERS_HPP -#define _HANDLERS_HPP - -#include - -#include -#include -#include -#include - -#include "bserv/common.hpp" - -// register an orm mapping (to convert the db query results into -// json objects). -// the db query results contain several rows, each has a number of -// fields. the order of `make_db_field(name[i])` in the -// initializer list corresponds to these fields (`Type[0]` and -// `name[0]` correspond to field[0], `Type[1]` and `name[1]` -// correspond to field[1], ...). `Type[i]` is the type you want -// to convert the field value to, and `name[i]` is the identifier -// with which you want to store the field in the json object, so -// if the returned json object is `obj`, `obj[name[i]]` will have -// the type of `Type[i]` and store the value of field[i]. -bserv::db_relation_to_object orm_user{ - bserv::make_db_field("id"), - bserv::make_db_field("username"), - bserv::make_db_field("password"), - bserv::make_db_field("is_superuser"), - bserv::make_db_field("first_name"), - bserv::make_db_field("last_name"), - bserv::make_db_field("email"), - bserv::make_db_field("is_active") -}; - -std::optional get_user( - bserv::db_transaction& tx, - const std::string& username) { - bserv::db_result r = tx.exec( - "select * from auth_user where username = ?", username); - lginfo << r.query(); // this is how you log info - return orm_user.convert_to_optional(r); -} - -std::string get_or_empty( - boost::json::object& obj, - const std::string& key) { - return obj.count(key) ? obj[key].as_string().c_str() : ""; -} - -// if you want to manually modify the response, -// the return type should be `std::nullopt_t`, -// and the return value should be `std::nullopt`. -std::nullopt_t hello( - bserv::response_type& response, - std::shared_ptr session_ptr) { - bserv::session_type& session = *session_ptr; - boost::json::object obj; - if (session.count("user")) { - // NOTE: modifications to sessions must be performed - // BEFORE referencing objects in them. this is because - // modifications might invalidate referenced objects. - // in this example, "count" might be added to `session`, - // which should be performed first. - // then `user` can be referenced safely. - if (!session.count("count")) { - session["count"] = 0; - } - auto& user = session["user"].as_object(); - session["count"] = session["count"].as_int64() + 1; - obj = { - {"welcome", user["username"]}, - {"count", session["count"]} - }; - } else { - obj = {{"msg", "hello, world!"}}; - } - // the response body is a string, - // so the `obj` should be serialized - response.body() = boost::json::serialize(obj); - response.prepare_payload(); // this line is important! - return std::nullopt; -} - -// if you return a json object, the serialization -// is performed automatically. -boost::json::object user_register( - bserv::request_type& request, - // the json object is obtained from the request body, - // as well as the url parameters - boost::json::object&& params, - std::shared_ptr conn) { - if (request.method() != boost::beast::http::verb::post) { - throw bserv::url_not_found_exception{}; - } - if (params.count("username") == 0) { - return { - {"success", false}, - {"message", "`username` is required"} - }; - } - if (params.count("password") == 0) { - return { - {"success", false}, - {"message", "`password` is required"} - }; - } - auto username = params["username"].as_string(); - bserv::db_transaction tx{conn}; - auto opt_user = get_user(tx, username.c_str()); - if (opt_user.has_value()) { - return { - {"success", false}, - {"message", "`username` existed"} - }; - } - auto password = params["password"].as_string(); - bserv::db_result r = tx.exec( - "insert into ? " - "(?, password, is_superuser, " - "first_name, last_name, email, is_active) values " - "(?, ?, ?, ?, ?, ?, ?)", bserv::db_name("auth_user"), - bserv::db_name("username"), - username.c_str(), - bserv::utils::security::encode_password( - password.c_str()), false, - get_or_empty(params, "first_name"), - get_or_empty(params, "last_name"), - get_or_empty(params, "email"), true); - lginfo << r.query(); - tx.commit(); // you must manually commit changes - return { - {"success", true}, - {"message", "user registered"} - }; -} - -boost::json::object user_login( - bserv::request_type& request, - boost::json::object&& params, - std::shared_ptr conn, - std::shared_ptr session_ptr) { - if (request.method() != boost::beast::http::verb::post) { - throw bserv::url_not_found_exception{}; - } - if (params.count("username") == 0) { - return { - {"success", false}, - {"message", "`username` is required"} - }; - } - if (params.count("password") == 0) { - return { - {"success", false}, - {"message", "`password` is required"} - }; - } - auto username = params["username"].as_string(); - bserv::db_transaction tx{conn}; - auto opt_user = get_user(tx, username.c_str()); - if (!opt_user.has_value()) { - return { - {"success", false}, - {"message", "invalid username/password"} - }; - } - auto& user = opt_user.value(); - if (!user["is_active"].as_bool()) { - return { - {"success", false}, - {"message", "invalid username/password"} - }; - } - auto password = params["password"].as_string(); - auto encoded_password = user["password"].as_string(); - if (!bserv::utils::security::check_password( - password.c_str(), encoded_password.c_str())) { - return { - {"success", false}, - {"message", "invalid username/password"} - }; - } - bserv::session_type& session = *session_ptr; - session["user"] = user; - return { - {"success", true}, - {"message", "login successfully"} - }; -} - -boost::json::object find_user( - std::shared_ptr conn, - const std::string& username) { - bserv::db_transaction tx{conn}; - auto user = get_user(tx, username); - if (!user.has_value()) { - return { - {"success", false}, - {"message", "requested user does not exist"} - }; - } - user.value().erase("id"); - user.value().erase("password"); - return { - {"success", true}, - {"user", user.value()} - }; -} - -boost::json::object user_logout( - std::shared_ptr session_ptr) { - bserv::session_type& session = *session_ptr; - if (session.count("user")) { - session.erase("user"); - } - return { - {"success", true}, - {"message", "logout successfully"} - }; -} - -boost::json::object send_request( - std::shared_ptr session, - std::shared_ptr client_ptr, - boost::json::object&& params) { - // post for response: - // auto res = client_ptr->post( - // "localhost", "8080", "/echo", {{"msg", "request"}} - // ); - // return {{"response", boost::json::parse(res.body())}}; - // ------------------------------------------------------- - // - if it takes longer than 30 seconds (by default) to - // - get the response, this will raise a read timeout - // ------------------------------------------------------- - // post for json response (json value, rather than json - // object, is returned): - auto obj = client_ptr->post_for_value( - "localhost", "8080", "/echo", {{"request", params}} - ); - if (session->count("cnt") == 0) { - (*session)["cnt"] = 0; - } - (*session)["cnt"] = (*session)["cnt"].as_int64() + 1; - return {{"response", obj}, {"cnt", (*session)["cnt"]}}; -} - -boost::json::object echo( - boost::json::object&& params) { - return {{"echo", params}}; -} - -// websocket -std::nullopt_t ws_echo( - std::shared_ptr session, - std::shared_ptr ws_server) { - ws_server->write_json((*session)["cnt"]); - while (true) { - try { - std::string data = ws_server->read(); - ws_server->write(data); - } catch (bserv::websocket_closed&) { - break; - } - } - return std::nullopt; -} - -#endif // _HANDLERS_HPP \ No newline at end of file diff --git a/main.cpp b/main.cpp deleted file mode 100644 index f21e6a8..0000000 --- a/main.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include -#include - -#include "bserv/common.hpp" -#include "handlers.hpp" - -void show_usage(const bserv::server_config& config) { - std::cout << "Usage: " << config.get_name() << " [OPTION...]\n" - << config.get_name() << " is a C++ Boost-based HTTP server.\n\n" - "Example:\n" - << " " << config.get_name() << " -p 8081 --threads 2\n\n" - "Option:\n" - " -h, --help show help and exit\n" - " -p, --port port (default: 8080)\n" - " --threads number of threads (default: # of cpu cores)\n" - " --rotation log rotation size in mega bytes (default: 8)\n" - " --log-path log path (default: ./log/bserv)\n" - " --num-conn number of database connections (default: 10)\n" - " -c, --conn-str connection string (default: dbname=bserv)" - << std::endl; -} - -// returns `true` if error occurs -bool parse_arguments(int argc, char* argv[], bserv::server_config& config) { - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { - show_usage(config); - return true; - } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { - if (i + 1 < argc) { - config.set_port(atoi(argv[i + 1])); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--threads") == 0) { - if (i + 1 < argc) { - config.set_num_threads(atoi(argv[i + 1])); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--num-conn") == 0) { - if (i + 1 < argc) { - config.set_num_db_conn(atoi(argv[i + 1])); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--rotation") == 0) { - if (i + 1 < argc) { - config.set_log_rotation_size(atoi(argv[i + 1]) * 1024 * 1024); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--log-path") == 0) { - if (i + 1 < argc) { - config.set_log_path(argv[i + 1]); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--conn-str") == 0) { - if (i + 1 < argc) { - config.set_db_conn_str(argv[i + 1]); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else { - std::cerr << "Unrecognized option: " << argv[i] << '\n' << std::endl; - show_usage(config); - return true; - } - } - return false; -} - -void show_config(const bserv::server_config& config) { - std::cout << config.get_name() << " config:" - << "\nport: " << config.get_port() - << "\nthreads: " << config.get_num_threads() - << "\nrotation: " << config.get_log_rotation_size() / 1024 / 1024 - << "\nlog path: " << config.get_log_path() - << "\ndb-conn: " << config.get_num_db_conn() - << "\nconn-str: " << config.get_db_conn_str() << std::endl; -} - -int main(int argc, char* argv[]) { - bserv::server_config config; - - if (parse_arguments(argc, argv, config)) - return EXIT_FAILURE; - - show_config(config); - - bserv::server{config, { - bserv::make_path("/", &hello, - bserv::placeholders::response, - bserv::placeholders::session), - bserv::make_path("/register", &user_register, - bserv::placeholders::request, - bserv::placeholders::json_params, - bserv::placeholders::db_connection_ptr), - bserv::make_path("/login", &user_login, - bserv::placeholders::request, - bserv::placeholders::json_params, - bserv::placeholders::db_connection_ptr, - bserv::placeholders::session), - bserv::make_path("/logout", &user_logout, - bserv::placeholders::session), - bserv::make_path("/find/", &find_user, - bserv::placeholders::db_connection_ptr, - bserv::placeholders::_1), - bserv::make_path("/send", &send_request, - bserv::placeholders::session, - bserv::placeholders::http_client_ptr, - bserv::placeholders::json_params), - bserv::make_path("/echo", &echo, - bserv::placeholders::json_params) - } - , { - bserv::make_path("/echo", &ws_echo, - bserv::placeholders::session, - bserv::placeholders::websocket_server_ptr) - } - }; - - return EXIT_SUCCESS; -} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..c8e7a89 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,101 @@ + + + + + + + + + WebApp - {% block title %}{% endblock %} + + + + + + + + + +
+ + + + {% if exists("message") %} + {% if exists("success") %} + {% if success %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endif %} + +
+ + {% block content %}{% endblock %} + +
+ © 2021 +
+
+
+ + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..69d63e5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}Home{% endblock %} + +{% block home_active %}active{% endblock %} + +{% block content %} +
+
+

Custom jumbotron

+

Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.

+ +
+
+ +
+
+
+

Change the background

+

Swap the background-color utility and add a `.text-*` color utility to mix up the jumbotron look. Then, mix and match with additional component themes and more.

+ +
+
+
+
+

Add borders

+

Or, keep it light and add a border for some added definition to the boundaries of your content. Be sure to look under the hood at the source HTML here as we've adjusted the alignment and sizing of both column's content for equal-height.

+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/statics/css/bootstrap.min.css b/templates/statics/css/bootstrap.min.css new file mode 100644 index 0000000..1472dec --- /dev/null +++ b/templates/statics/css/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/templates/statics/css/bootstrap.min.css.map b/templates/statics/css/bootstrap.min.css.map new file mode 100644 index 0000000..c84afa4 --- /dev/null +++ b/templates/statics/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.3 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$variable-prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-` + + +

+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
#UsernameFirstLastEmail
{{ loop.index1 }}{{ user.username }}{{ user.first_name }}{{ user.last_name }}{{ user.email }}
+ +{% if exists("pagination") %} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/scripts/db_test.py b/test/scripts/db_test.py similarity index 79% rename from scripts/db_test.py rename to test/scripts/db_test.py index 728ebe9..f7a73ab 100644 --- a/scripts/db_test.py +++ b/test/scripts/db_test.py @@ -35,7 +35,7 @@ def create_user(): def session_test(): session = requests.session() user = create_user() - res = session.post("http://localhost:8080").json() + res = session.post("http://localhost:8080/hello").json() if res != {'msg': 'hello, world!'}: print('test failed') # print(res) @@ -52,7 +52,7 @@ def session_test(): # print(res) n = random.randint(1, 5) for i in range(1, n + 1): - res = session.post("http://localhost:8080").json() + res = session.post("http://localhost:8080/hello").json() if res != {'welcome': user["username"], 'count': i}: print('test failed') # print(res) @@ -73,29 +73,31 @@ def session_test(): if res != {'success': True, 'message': 'logout successfully'}: print('test failed') # print(res) - res = session.post("http://localhost:8080").json() + res = session.post("http://localhost:8080/hello").json() if res != {'msg': 'hello, world!'}: print('test failed') # print(res) -# session_test() +# for i in range(100): +# session_test() # exit() -P = 1000 # number of concurrent processes +if __name__ == '__main__': + P = 100 # number of concurrent processes -processes = [Process(target=session_test) for i in range(P)] + processes = [Process(target=session_test) for _ in range(P)] -print('starting') + print('starting') -start = time() + start = time() -for p in processes: - p.start() + for p in processes: + p.start() -for p in processes: - p.join() + for p in processes: + p.join() -end = time() + end = time() -print('test ended') -print('elapsed: ', end - start) + print('test ended') + print('elapsed: ', end - start) diff --git a/scripts/request_test.py b/test/scripts/request_test.py similarity index 69% rename from scripts/request_test.py rename to test/scripts/request_test.py index 9bf9f8d..b9b05ad 100644 --- a/scripts/request_test.py +++ b/test/scripts/request_test.py @@ -19,11 +19,8 @@ def size_test(): print("size test: ok") print() -size_test() -# exit() - -P = 500 # number of concurrent processes -N = 10 # for each process, the number of sessions +P = 100 # number of concurrent processes +N = 5 # for each process, the number of sessions R = 10 # for each session, the number of posts def test(i): @@ -38,19 +35,24 @@ def test(i): print('test failed!') # print(f'exiting process {i}') -processes = [Process(target=test, args=(i, )) for i in range(P)] -print('starting') +if __name__ == '__main__': + size_test() + # exit() -start = time() + processes = [Process(target=test, args=(i, )) for i in range(P)] -for p in processes: - p.start() + print('starting') -for p in processes: - p.join() + start = time() -end = time() + for p in processes: + p.start() -print('test ended') -print('elapsed: ', end - start) + for p in processes: + p.join() + + end = time() + + print('test ended') + print('elapsed: ', end - start) diff --git a/scripts/ws_test.py b/test/scripts/ws_test.py similarity index 82% rename from scripts/ws_test.py rename to test/scripts/ws_test.py index 9cc440b..56770f4 100644 --- a/scripts/ws_test.py +++ b/test/scripts/ws_test.py @@ -10,7 +10,7 @@ from time import time from pprint import pprint -P = 500 +P = 100 def test(): @@ -44,19 +44,20 @@ def test(): fun("ws://localhost:8080/echo") ) -processes = [Process(target=test) for _ in range(P)] +if __name__ == '__main__': + processes = [Process(target=test) for _ in range(P)] -print('starting') + print('starting') -start = time() + start = time() -for p in processes: - p.start() + for p in processes: + p.start() -for p in processes: - p.join() + for p in processes: + p.join() -end = time() + end = time() -print('test ended') -print('elapsed: ', end - start) + print('test ended') + print('elapsed: ', end - start)