project has been restructured to adapt to windows

This commit is contained in:
jie 2021-10-18 13:59:21 +08:00
parent b6700ccc0b
commit 050a475304
63 changed files with 3835 additions and 2592 deletions

9
.gitignore vendored
View File

@ -1,7 +1,8 @@
test/
build/*
!build/README.md
.*
.vscode
.vs
x64
log
pgsql
# Prerequisites
*.d

12
.gitmodules vendored Normal file
View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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/<user>`|958.77|
For `/login`, we intentionally slow down the attacker's speed.
### Computer Hardware:
- Intel Core i9-9900K x 4
- 16GB RAM

44
WebApp/WebApp.sln Normal file
View File

@ -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

137
WebApp/WebApp/WebApp.cpp Normal file
View File

@ -0,0 +1,137 @@
#include <iostream>
#include <cstdlib>
#include <string>
#include <boost/json.hpp>
#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/<str>", &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/<path>", &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/<int>", &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;
}

View File

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{d46cb9a5-375a-470b-b9b0-1353862a18f5}</ProjectGuid>
<RootNamespace>WebApp</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>..\..\dependencies\inja\include;..\..\dependencies\inja\third_party\include;..\bserv\include;..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/bigobj %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(OutDir);..\..\dependencies\libpqxx\src\Debug;..\..\dependencies\pgsql\lib;..\..\dependencies\cryptopp\x64\Output\Debug;..\..\dependencies\boost\stage\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>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)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>xcopy /y /d "..\..\dependencies\pgsql\bin\*.dll" "$(OutDir)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>..\..\dependencies\inja\include;..\..\dependencies\inja\third_party\include;..\bserv\include;..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(OutDir);..\..\dependencies\libpqxx\src\Release;..\..\dependencies\pgsql\lib;..\..\dependencies\cryptopp\x64\Output\Release;..\..\dependencies\boost\stage\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>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)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>xcopy /y /d "..\..\dependencies\pgsql\bin\*.dll" "$(OutDir)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="handlers.cpp" />
<ClCompile Include="rendering.cpp" />
<ClCompile Include="WebApp.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="handlers.h" />
<ClInclude Include="rendering.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="WebApp.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="rendering.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="handlers.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="handlers.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="rendering.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LocalDebuggerCommandArguments>..\..\config.json</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LocalDebuggerCommandArguments>..\..\config.json</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>

390
WebApp/WebApp/handlers.cpp Normal file
View File

@ -0,0 +1,390 @@
#include "handlers.h"
#include <vector>
#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<Type[i]>(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<int>("id"),
bserv::make_db_field<std::string>("username"),
bserv::make_db_field<std::string>("password"),
bserv::make_db_field<bool>("is_superuser"),
bserv::make_db_field<std::string>("first_name"),
bserv::make_db_field<std::string>("last_name"),
bserv::make_db_field<std::string>("email"),
bserv::make_db_field<bool>("is_active")
};
std::optional<boost::json::object> 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<bserv::session_type> 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<bserv::db_connection> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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<bserv::db_connection> 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<bserv::session_type> 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<bserv::session_type> session,
std::shared_ptr<bserv::http_client> 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<bserv::session_type> session,
std::shared_ptr<bserv::websocket_server> 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<bserv::session_type> 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<bserv::session_type> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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<bserv::session_type> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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<std::size_t>();
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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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));
}

76
WebApp/WebApp/handlers.h Normal file
View File

@ -0,0 +1,76 @@
#pragma once
#include <boost/json.hpp>
#include <string>
#include <memory>
#include <optional>
#include "bserv/common.hpp"
std::nullopt_t hello(
bserv::response_type& response,
std::shared_ptr<bserv::session_type> session_ptr);
boost::json::object user_register(
bserv::request_type& request,
boost::json::object&& params,
std::shared_ptr<bserv::db_connection> conn);
boost::json::object user_login(
bserv::request_type& request,
boost::json::object&& params,
std::shared_ptr<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> session_ptr);
boost::json::object find_user(
std::shared_ptr<bserv::db_connection> conn,
const std::string& username);
boost::json::object user_logout(
std::shared_ptr<bserv::session_type> session_ptr);
boost::json::object send_request(
std::shared_ptr<bserv::session_type> session,
std::shared_ptr<bserv::http_client> client_ptr,
boost::json::object&& params);
boost::json::object echo(
boost::json::object&& params);
// websocket
std::nullopt_t ws_echo(
std::shared_ptr<bserv::session_type> session,
std::shared_ptr<bserv::websocket_server> ws_server);
std::nullopt_t serve_static_files(
bserv::response_type& response,
const std::string& path);
std::nullopt_t index(
std::shared_ptr<bserv::session_type> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> session_ptr);
std::nullopt_t form_logout(
std::shared_ptr<bserv::session_type> session_ptr,
bserv::response_type& response);
std::nullopt_t view_users(
std::shared_ptr<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> session_ptr);

View File

@ -0,0 +1,84 @@
#include "rendering.h"
#include <fstream>
#include <boost/beast.hpp>
#include <inja/inja.hpp>
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;
}

24
WebApp/WebApp/rendering.h Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <optional>
#include <boost/json.hpp>
#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
);

511
WebApp/bserv/bserv.cpp Normal file
View File

@ -0,0 +1,511 @@
#include "pch.h"
#include "framework.h"
#include <iostream>
#include <string>
#include <cstddef>
#include <cstdlib>
#include <vector>
#include <optional>
#include <functional>
#include <thread>
#include <chrono>
#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<http::string_body> handle_request(
http::request<http::string_body>& req, router& routes,
std::shared_ptr<websocket_session> ws_session,
asio::io_context& ioc, asio::yield_context& yield) {
const auto bad_request = [&req](beast::string_view why) {
http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<boost::json::value> 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<websocket_session_server>,
std::shared_ptr<websocket_session> session,
http::request<http::string_body>& req, router& routes,
asio::io_context& ioc, asio::yield_context yield);
class websocket_session_server
: public std::enable_shared_from_this<websocket_session_server> {
private:
friend websocket_server;
std::string address_;
std::shared_ptr<websocket_session> session_;
http::request<http::string_body> 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<http::string_body>&& 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<websocket_session_server>,
std::shared_ptr<websocket_session> session,
http::request<http::string_body>& 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 <class Send>
void handle_http_request(
std::shared_ptr<http_session>,
http::request<http::string_body> 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<http_session> {
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 <bool isRequest, class Body, class Fields>
void operator()(
http::message<isRequest, Body, Fields>&& 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<isRequest, Body, Fields>>(
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<http::string_body>> parser_;
std::shared_ptr<void> 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<websocket_session_server>(
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<send_lambda>,
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<listener> {
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<http_session>(
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<memory_session_manager>();
std::shared_ptr<server_resources> resources_ptr = std::make_shared<server_resources>();
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<listener>(
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<std::thread> 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

190
WebApp/bserv/bserv.vcxproj Normal file
View File

@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{f5c0cf6d-7bf9-40a5-af2e-8fc36a1d7296}</ProjectGuid>
<RootNamespace>bserv</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>C:\Users\jiesh\Desktop\projects\bserv\include;include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>
</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>
</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalOptions>/bigobj %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>
</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>..\..\dependencies\libpqxx\include;..\..\dependencies;..\..\dependencies\boost;include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>
</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="framework.h" />
<ClInclude Include="include\bserv\client.hpp" />
<ClInclude Include="include\bserv\common.hpp" />
<ClInclude Include="include\bserv\config.hpp" />
<ClInclude Include="include\bserv\database.hpp" />
<ClInclude Include="include\bserv\logging.hpp" />
<ClInclude Include="include\bserv\router.hpp" />
<ClInclude Include="include\bserv\server.hpp" />
<ClInclude Include="include\bserv\session.hpp" />
<ClInclude Include="include\bserv\utils.hpp" />
<ClInclude Include="include\bserv\websocket.hpp" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="bserv.cpp" />
<ClCompile Include="client.cpp" />
<ClCompile Include="database.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="session.cpp" />
<ClCompile Include="utils.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="framework.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\client.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\common.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\config.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\database.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\logging.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\router.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\server.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\session.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\utils.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="include\bserv\websocket.hpp">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="bserv.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="client.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="database.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="session.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="utils.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

75
WebApp/bserv/client.cpp Normal file
View File

@ -0,0 +1,75 @@
#include "pch.h"
#include "bserv/client.hpp"
#include "bserv/logging.hpp"
#include <chrono>
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::string_body> http_client_send(
asio::io_context& ioc,
asio::yield_context& yield,
const std::string& host,
const std::string& port,
const http::request<http::string_body>& 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<http::string_body> 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

36
WebApp/bserv/database.cpp Normal file
View File

@ -0,0 +1,36 @@
#include "pch.h"
#include "bserv/database.hpp"
namespace bserv {
std::shared_ptr<db_connection> 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<std::mutex> lg{ queue_lock_ };
std::shared_ptr<raw_db_connection_type> 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<db_connection>(*this, conn);
}
db_connection::~db_connection() {
std::lock_guard<std::mutex> 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

3
WebApp/bserv/framework.h Normal file
View File

@ -0,0 +1,3 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容

View File

@ -0,0 +1,145 @@
#ifndef _CLIENT_HPP
#define _CLIENT_HPP
#include <boost/beast.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio.hpp>
#include <boost/json.hpp>
#include <iostream>
#include <string>
#include <exception>
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<http::string_body>;
using response_type = http::response<http::string_body>;
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::string_body> http_client_send(
asio::io_context& ioc,
asio::yield_context& yield,
const std::string& host,
const std::string& port,
const http::request<http::string_body>& 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<http::string_body> request(
const std::string& host,
const std::string& port,
const http::request<http::string_body>& 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<http::string_body>& 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

View File

@ -1,6 +1,8 @@
#ifndef _COMMON_HPP
#define _COMMON_HPP
#define _WIN32_WINNT 0x0601
#include "client.hpp"
#include "config.hpp"
#include "database.hpp"

View File

@ -0,0 +1,50 @@
#ifndef _CONFIG_HPP
#define _CONFIG_HPP
#include <iostream>
#include <string>
#include <cstddef>
#include <optional>
#include <thread>
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<type> name##_; \
public: \
void set_##name(std::optional<type>&& 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

View File

@ -0,0 +1,355 @@
#ifndef _DATABASE_HPP
#define _DATABASE_HPP
#include <boost/json.hpp>
#include <cstddef>
#include <string>
#include <vector>
#include <queue>
#include <optional>
#include <mutex>
#include <memory>
#include <initializer_list>
#include <pqxx/pqxx>
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 <typename Type>
Type as() const { return field_.as<Type>(); }
};
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<raw_db_connection_type> conn_;
public:
db_connection(
db_connection_manager& mgr,
std::shared_ptr<raw_db_connection_type> 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<std::shared_ptr<raw_db_connection_type>> 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<raw_db_connection_type>(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<db_connection> 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 <typename Type>
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<std::string> : 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<bool> : 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 <typename Type>
class db_value<std::vector<Type>> : public db_parameter {
private:
std::vector<Type> value_;
public:
db_value(const std::vector<Type>& 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<Type>{elem}.get_value(tx);
}
return "ARRAY[" + res + "]";
}
};
namespace db_internal {
template <typename Param>
std::shared_ptr<db_parameter> convert_parameter(
const Param& param) {
return std::make_shared<db_value<Param>>(param);
}
template <typename Param>
std::shared_ptr<db_parameter> convert_parameter(
const db_value<Param>& param) {
return std::make_shared<db_value<Param>>(param);
}
inline std::shared_ptr<db_parameter> convert_parameter(
const char* param) {
return std::make_shared<db_value<std::string>>(param);
}
inline std::shared_ptr<db_parameter> convert_parameter(
const db_name& param) {
return std::make_shared<db_name>(param);
}
template <typename ...Params>
std::vector<std::string> convert_parameters(
raw_db_transaction_type& tx, std::shared_ptr<Params>... 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 <typename Type>
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<Type>();
}
};
template <>
class db_field<std::string> : 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 <typename Type>
std::shared_ptr<db_internal::db_field_holder> make_db_field(
const std::string& name) {
return std::make_shared<db_internal::db_field<Type>>(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<std::shared_ptr<db_internal::db_field_holder>> fields_;
public:
db_relation_to_object(
const std::initializer_list<
std::shared_ptr<db_internal::db_field_holder>>&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<boost::json::object> convert_to_vector(
const db_result& result) {
std::vector<boost::json::object> results;
for (const auto& row : result)
results.emplace_back(convert_row(row));
return results;
}
std::optional<boost::json::object> 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<db_connection> 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<bool>(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 <typename ...Params>
db_result exec(const std::string& s, const Params&... params) {
std::vector<std::string> 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

View File

@ -0,0 +1,57 @@
#ifndef _LOGGING_HPP
#define _LOGGING_HPP
//#define BOOST_LOG_DYN_LINK
#include <boost/log/core.hpp>
#include <boost/log/common.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/utility/setup.hpp>
#include <iostream>
#include <cstddef>
#include <string>
#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

View File

@ -0,0 +1,431 @@
#ifndef _ROUTER_HPP
#define _ROUTER_HPP
#include <boost/asio/spawn.hpp>
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/json.hpp>
#include <string>
#include <regex>
#include <vector>
#include <map>
#include <memory>
#include <initializer_list>
#include <optional>
#include <pqxx/pqxx>
#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_manager_base> session_mgr;
std::shared_ptr<db_connection_manager> db_conn_mgr;
};
struct request_resources {
server_resources& resources;
asio::io_context& ioc;
asio::yield_context& yield;
std::shared_ptr<websocket_session> ws_session;
const std::vector<std::string>& url_params;
request_type& request;
response_type& response;
std::shared_ptr<session_type> session_ptr;
std::shared_ptr<db_connection> db_connection_ptr;
std::shared_ptr<http_client> http_client_ptr;
std::shared_ptr<websocket_server> websocket_server_ptr;
};
namespace placeholders {
template <int N>
struct placeholder {};
#define make_place_holder(x) constexpr placeholder<x> _##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<bserv::session_type>
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<bserv::db_connection>
constexpr placeholder<-5> db_connection_ptr;
// std::shared_ptr<bserv::http_client>
constexpr placeholder<-6> http_client_ptr;
// std::shared_ptr<bserv::websocket_server>
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 <typename ...Types>
struct parameter_pack;
template <>
struct parameter_pack<> {};
template <typename Head, typename ...Tail>
struct parameter_pack<Head, Tail...>
: parameter_pack<Tail...> {
Head head_;
template <typename Head2, typename ...Tail2>
parameter_pack(Head2&& head, Tail2&& ...tail)
: parameter_pack<Tail...>{ static_cast<Tail2&&>(tail)... },
head_{ static_cast<Head2&&>(head) } {}
};
template <int Idx, typename ...Types>
struct get_parameter_pack;
template <int Idx, typename Head, typename ...Tail>
struct get_parameter_pack<Idx, Head, Tail...>
: get_parameter_pack<Idx - 1, Tail...> {};
template <typename Head, typename ...Tail>
struct get_parameter_pack<0, Head, Tail...> {
using type = parameter_pack<Head, Tail...>;
};
template <int Idx, typename ...Types>
decltype(auto) get_parameter_value(parameter_pack<Types...>& params) {
return (static_cast<
typename get_parameter_pack<Idx, Types...>::type&
>(params).head_);
}
template <int Idx, typename ...Types>
struct get_parameter;
template <int Idx, typename Head, typename ...Tail>
struct get_parameter<Idx, Head, Tail...>
: get_parameter<Idx - 1, Tail...> {};
template <typename Head, typename ...Tail>
struct get_parameter<0, Head, Tail...> {
using type = Head;
};
template <typename Type>
Type&& get_parameter_data(
request_resources&, Type&& val) {
return static_cast<Type&&>(val);
}
template <int N, std::enable_if_t<(N >= 0), int> = 0>
const std::string& get_parameter_data(
request_resources& resources,
placeholders::placeholder<N>) {
return resources.url_params[N];
}
inline std::shared_ptr<session_type> 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_type> 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<std::string, std::string>& dict_param,
const std::map<std::string, std::vector<std::string>>& 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<db_connection> 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<http_client> get_parameter_data(
request_resources& resources,
placeholders::placeholder<-6>) {
if (resources.http_client_ptr == nullptr)
resources.http_client_ptr =
std::make_shared<http_client>(resources.ioc, resources.yield);
return resources.http_client_ptr;
}
inline std::shared_ptr<websocket_server> get_parameter_data(
request_resources& resources,
placeholders::placeholder<-7>) {
if (resources.websocket_server_ptr == nullptr)
resources.websocket_server_ptr =
std::make_shared<websocket_server>(*resources.ws_session, resources.yield);
return resources.websocket_server_ptr;
}
template <int Idx, typename Func, typename Params, typename ...Args>
struct path_handler;
template <int Idx, typename Ret, typename ...Args, typename ...Params>
struct path_handler<Idx, Ret(*)(Args ...), parameter_pack<Params...>> {
Ret invoke(request_resources& resources,
Ret(*pf)(Args ...), parameter_pack<Params...>& params) {
// suppress msvc warning
boost::ignore_unused(params);
if constexpr (Idx == 0) return (*pf)();
else return static_cast<path_handler<
Idx - 1, Ret(*)(Args ...), parameter_pack<Params...>,
typename get_parameter<Idx - 1, Params...>::type>*
>(this)->invoke2(resources, pf, params,
get_parameter_data(resources,
get_parameter_value<Idx - 1>(params)));
}
};
template <int Idx, typename Ret, typename ...Args,
typename ...Params, typename Head, typename ...Tail>
struct path_handler<Idx, Ret(*)(Args ...),
parameter_pack<Params...>, Head, Tail...>
: path_handler<Idx + 1, Ret(*)(Args ...),
parameter_pack<Params...>, Tail...> {
template <
typename Head2, typename ...Tail2,
std::enable_if_t<sizeof...(Tail2) == sizeof...(Tail), int> = 0>
Ret invoke2(request_resources& resources,
Ret(*pf)(Args ...), parameter_pack<Params...>& params,
Head2&& head2, Tail2&& ...tail2) {
// suppress msvc warning
boost::ignore_unused(params);
if constexpr (Idx == 0)
return (*pf)(static_cast<Head2&&>(head2),
static_cast<Tail2&&>(tail2)...);
else return static_cast<path_handler<
Idx - 1, Ret(*)(Args ...), parameter_pack<Params...>,
typename get_parameter<Idx - 1, Params...>::type, Head, Tail...>*
>(this)->invoke2(resources, pf, params,
get_parameter_data(resources,
get_parameter_value<Idx - 1>(params)),
static_cast<Head2&&>(head2), static_cast<Tail2&&>(tail2)...);
}
};
const std::vector<std::pair<std::regex, std::string>> url_regex_mapping{
{std::regex{"<int>"}, "([0-9]+)"},
{std::regex{"<str>"}, R"(([A-Za-z0-9_\.\-]+))"},
{std::regex{"<path>"}, 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> {
path_holder() = default;
virtual ~path_holder() = default;
virtual bool match(
const std::string&,
std::vector<std::string>&) const = 0;
virtual std::optional<boost::json::value> invoke(
request_resources&) = 0;
};
template <typename Func, typename Params>
class path;
template <typename Ret, typename ...Args, typename ...Params>
class path<Ret(*)(Args ...), parameter_pack<Params...>>
: public path_holder {
private:
std::regex re_;
Ret(*pf_)(Args ...);
parameter_pack<Params...> params_;
path_handler<0, Ret(*)(Args ...), parameter_pack<Params...>, Params...> handler_;
public:
path(const std::string& url, Ret(*pf)(Args ...), Params&& ...params)
: re_{ get_re_url(url) }, pf_{ pf },
params_{ static_cast<Params&&>(params)... } {}
bool match(const std::string& url, std::vector<std::string>& 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<boost::json::value> invoke(
request_resources& resources) {
return handler_.invoke(
resources, pf_, params_);
}
};
} // router_internal
template <typename Ret, typename ...Args, typename ...Params>
std::shared_ptr<router_internal::path<Ret(*)(Args ...),
router_internal::parameter_pack<Params...>>> make_path(
const std::string& url, Ret(*pf)(Args ...), Params&& ...params) {
return std::make_shared<
router_internal::path<Ret(*)(Args ...),
router_internal::parameter_pack<Params...>>
>(url, pf, static_cast<Params&&>(params)...);
}
template <typename Ret, typename ...Args, typename ...Params>
std::shared_ptr<router_internal::path<Ret(*)(Args ...),
router_internal::parameter_pack<Params...>>> make_path(
const char* url, Ret(*pf)(Args ...), Params&& ...params) {
return std::make_shared<
router_internal::path<Ret(*)(Args ...),
router_internal::parameter_pack<Params...>>
>(url, pf, static_cast<Params&&>(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<router_internal::path_holder>;
std::vector<path_holder_type> paths_;
std::shared_ptr<server_resources> resources_;
public:
router(const std::initializer_list<path_holder_type>& paths)
: paths_{ paths } {}
void set_resources(std::shared_ptr<server_resources> resources) {
resources_ = resources;
}
std::optional<boost::json::value> operator()(
asio::io_context& ioc, asio::yield_context& yield,
std::shared_ptr<websocket_session> ws_session,
const std::string& url, request_type& request, response_type& response) {
std::vector<std::string> 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

View File

@ -0,0 +1,69 @@
#ifndef _SESSION_HPP
#define _SESSION_HPP
#include <boost/json.hpp>
#include <cstddef>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <mutex>
#include <memory>
#include <chrono>
#include <random>
#include <limits>
#include "utils.hpp"
namespace bserv {
const std::string SESSION_NAME = "bsessionid";
// using session_type = std::map<std::string, boost::json::value>;
using session_type = boost::json::object;
struct session_manager_base
: std::enable_shared_from_this<session_manager_base> {
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_type>& 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<std::size_t> dist_;
std::map<std::string, std::size_t> str_to_int_;
std::map<std::size_t, std::string> int_to_str_;
std::map<std::size_t, std::shared_ptr<session_type>> sessions_;
// `expiry` stores <key, expiry> tuple sorted by key
std::map<std::size_t, time_point> expiry_;
// `queue` functions as a priority queue
// (the front element is the smallest)
// and stores <expiry, key> tuple sorted by
// expiry first and then key.
std::set<std::pair<time_point, std::size_t>> queue_;
mutable std::mutex lock_;
public:
memory_session_manager()
: rng_{ utils::internal::get_rd_value() },
dist_{ 0, std::numeric_limits<std::size_t>::max() } {}
bool get_or_create(
std::string& key,
std::shared_ptr<session_type>& session_ptr);
};
} // bserv
#endif // _SESSION_HPP

View File

@ -0,0 +1,62 @@
#ifndef _UTILS_HPP
#define _UTILS_HPP
#include <cstddef>
#include <string>
#include <tuple>
#include <vector>
#include <map>
#include <random>
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::string, std::string>,
std::map<std::string, std::vector<std::string>>>
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::string,
std::map<std::string, std::string>,
std::map<std::string, std::vector<std::string>>>
parse_url(std::string& s);
} // bserv::utils
#endif // _UTILS_HPP

View File

@ -0,0 +1,65 @@
#ifndef _WEBSOCKET_HPP
#define _WEBSOCKET_HPP
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <boost/json.hpp>
#include <iostream>
#include <string>
#include <cstddef>
#include <cstdlib>
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<beast::tcp_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

5
WebApp/bserv/pch.cpp Normal file
View File

@ -0,0 +1,5 @@
// pch.cpp: 与预编译标头对应的源文件
#include "pch.h"
// 当使用预编译的头时,需要使用此源文件,编译才能成功。

15
WebApp/bserv/pch.h Normal file
View File

@ -0,0 +1,15 @@
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
// 添加要在此处预编译的标头
#include "framework.h"
#define _WIN32_WINNT 0x0601
#endif //PCH_H

50
WebApp/bserv/session.cpp Normal file
View File

@ -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_type>& session_ptr) {
std::lock_guard<std::mutex> 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<session_type>();
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

236
WebApp/bserv/utils.cpp Normal file
View File

@ -0,0 +1,236 @@
#include "pch.h"
#include "bserv/utils.hpp"
#include <mutex>
#include <sstream>
#include <iomanip>
#include <cryptopp/cryptlib.h>
#include <cryptopp/pwdbased.h>
#include <cryptopp/sha.h>
#include <cryptopp/base64.h>
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<std::mutex> 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<SHA256> 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::string, std::string>,
std::map<std::string, std::vector<std::string>>>
parse_params(std::string& s, std::size_t start_pos, char delimiter) {
std::map<std::string, std::string> dict_params;
std::map<std::string, std::vector<std::string>> 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::string,
std::map<std::string, std::string>,
std::map<std::string, std::vector<std::string>>>
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::string, std::string>{},
std::map<std::string, std::vector<std::string>>{});
auto&& [dict_params, list_params] = parse_params(s, i + 1);
return std::make_tuple(url, dict_params, list_params);
}
} // bserv::utils

View File

@ -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)

View File

@ -1,204 +0,0 @@
#ifndef _CLIENT_HPP
#define _CLIENT_HPP
#include <boost/beast.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio.hpp>
#include <boost/json.hpp>
#include <iostream>
#include <string>
#include <cstddef>
#include <future>
#include <memory>
#include <chrono>
#include <exception>
#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<http::string_body>;
using response_type = http::response<http::string_body>;
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::string_body> http_client_send(
asio::io_context& ioc,
asio::yield_context& yield,
const std::string& host,
const std::string& port,
const http::request<http::string_body>& 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<http::string_body> 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<http::string_body> request(
const std::string& host,
const std::string& port,
const http::request<http::string_body>& 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<http::string_body>& 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

View File

@ -1,50 +0,0 @@
#ifndef _CONFIG_HPP
#define _CONFIG_HPP
#include <iostream>
#include <string>
#include <cstddef>
#include <optional>
#include <thread>
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<type> name##_; \
public: \
void set_##name(std::optional<type>&& 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

View File

@ -1,380 +0,0 @@
#ifndef _DATABASE_HPP
#define _DATABASE_HPP
#include <boost/json.hpp>
#include <cstddef>
#include <string>
#include <vector>
#include <queue>
#include <optional>
#include <mutex>
#include <memory>
#include <initializer_list>
#include <pqxx/pqxx>
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 <typename Type>
Type as() const { return field_.as<Type>(); }
};
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<raw_db_connection_type> conn_;
public:
db_connection(
db_connection_manager& mgr,
std::shared_ptr<raw_db_connection_type> 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<std::shared_ptr<raw_db_connection_type>> 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<raw_db_connection_type>(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<db_connection> 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<std::mutex> lg{queue_lock_};
std::shared_ptr<raw_db_connection_type> 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<db_connection>(*this, conn);
}
};
inline db_connection::~db_connection() {
std::lock_guard<std::mutex> 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 <typename Type>
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<std::string> : 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<bool> : 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 <typename Type>
class db_value<std::vector<Type>> : public db_parameter {
private:
std::vector<Type> value_;
public:
db_value(const std::vector<Type>& 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<Type>{elem}.get_value(tx);
}
return "ARRAY[" + res + "]";
}
};
namespace db_internal {
template <typename Param>
std::shared_ptr<db_parameter> convert_parameter(
const Param& param) {
return std::make_shared<db_value<Param>>(param);
}
template <typename Param>
std::shared_ptr<db_parameter> convert_parameter(
const db_value<Param>& param) {
return std::make_shared<db_value<Param>>(param);
}
inline std::shared_ptr<db_parameter> convert_parameter(
const char* param) {
return std::make_shared<db_value<std::string>>(param);
}
inline std::shared_ptr<db_parameter> convert_parameter(
const db_name& param) {
return std::make_shared<db_name>(param);
}
template <typename ...Params>
std::vector<std::string> convert_parameters(
raw_db_transaction_type& tx, std::shared_ptr<Params>... 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 <typename Type>
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<Type>();
}
};
template <>
class db_field<std::string> : 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 <typename Type>
std::shared_ptr<db_internal::db_field_holder> make_db_field(
const std::string& name) {
return std::make_shared<db_internal::db_field<Type>>(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<std::shared_ptr<db_internal::db_field_holder>> fields_;
public:
db_relation_to_object(
const std::initializer_list<
std::shared_ptr<db_internal::db_field_holder>>& 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<boost::json::object> convert_to_vector(
const db_result& result) {
std::vector<boost::json::object> results;
for (const auto& row : result)
results.emplace_back(convert_row(row));
return results;
}
std::optional<boost::json::object> 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<db_connection> 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<bool>(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 <typename ...Params>
db_result exec(const std::string& s, const Params&... params) {
std::vector<std::string> 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

View File

@ -1,48 +0,0 @@
#ifndef _LOGGING_HPP
#define _LOGGING_HPP
#define BOOST_LOG_DYN_LINK
#include <boost/log/core.hpp>
#include <boost/log/common.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/utility/setup.hpp>
#include <cstddef>
#include <string>
#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

View File

@ -1,414 +0,0 @@
#ifndef _ROUTER_HPP
#define _ROUTER_HPP
#include <boost/asio/spawn.hpp>
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/json.hpp>
#include <string>
#include <regex>
#include <vector>
#include <map>
#include <memory>
#include <initializer_list>
#include <optional>
#include <pqxx/pqxx>
#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_manager_base> session_mgr;
std::shared_ptr<db_connection_manager> db_conn_mgr;
};
struct request_resources {
server_resources& resources;
asio::io_context& ioc;
asio::yield_context& yield;
std::shared_ptr<websocket_session> ws_session;
const std::vector<std::string>& url_params;
request_type& request;
response_type& response;
std::shared_ptr<session_type> session_ptr;
std::shared_ptr<db_connection> db_connection_ptr;
std::shared_ptr<http_client> http_client_ptr;
std::shared_ptr<websocket_server> websocket_server_ptr;
};
namespace placeholders {
template <int N>
struct placeholder {};
#define make_place_holder(x) constexpr placeholder<x> _##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<bserv::session_type>
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<bserv::db_connection>
constexpr placeholder<-5> db_connection_ptr;
// std::shared_ptr<bserv::http_client>
constexpr placeholder<-6> http_client_ptr;
// std::shared_ptr<bserv::websocket_server>
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 <typename ...Types>
struct parameter_pack;
template <>
struct parameter_pack<> {};
template <typename Head, typename ...Tail>
struct parameter_pack<Head, Tail...>
: parameter_pack<Tail...> {
Head head_;
template <typename Head2, typename ...Tail2>
parameter_pack(Head2&& head, Tail2&& ...tail)
: parameter_pack<Tail...>{static_cast<Tail2&&>(tail)...},
head_{static_cast<Head2&&>(head)} {}
};
template <int Idx, typename ...Types>
struct get_parameter_pack;
template <int Idx, typename Head, typename ...Tail>
struct get_parameter_pack<Idx, Head, Tail...>
: get_parameter_pack<Idx - 1, Tail...> {};
template <typename Head, typename ...Tail>
struct get_parameter_pack<0, Head, Tail...> {
using type = parameter_pack<Head, Tail...>;
};
template <int Idx, typename ...Types>
decltype(auto) get_parameter_value(parameter_pack<Types...>& params) {
return (static_cast<
typename get_parameter_pack<Idx, Types...>::type&
>(params).head_);
}
template <int Idx, typename ...Types>
struct get_parameter;
template <int Idx, typename Head, typename ...Tail>
struct get_parameter<Idx, Head, Tail...>
: get_parameter<Idx - 1, Tail...> {};
template <typename Head, typename ...Tail>
struct get_parameter<0, Head, Tail...> {
using type = Head;
};
template <typename Type>
Type&& get_parameter_data(
request_resources&, Type&& val) {
return static_cast<Type&&>(val);
}
template <int N, std::enable_if_t<(N >= 0), int> = 0>
const std::string& get_parameter_data(
request_resources& resources,
placeholders::placeholder<N>) {
return resources.url_params[N];
}
inline std::shared_ptr<session_type> 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_type> 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<db_connection> 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<http_client> get_parameter_data(
request_resources& resources,
placeholders::placeholder<-6>) {
if (resources.http_client_ptr == nullptr)
resources.http_client_ptr =
std::make_shared<http_client>(resources.ioc, resources.yield);
return resources.http_client_ptr;
}
inline std::shared_ptr<websocket_server> get_parameter_data(
request_resources& resources,
placeholders::placeholder<-7>) {
if (resources.websocket_server_ptr == nullptr)
resources.websocket_server_ptr =
std::make_shared<websocket_server>(*resources.ws_session, resources.yield);
return resources.websocket_server_ptr;
}
template <int Idx, typename Func, typename Params, typename ...Args>
struct path_handler;
template <int Idx, typename Ret, typename ...Args, typename ...Params>
struct path_handler<Idx, Ret (*)(Args ...), parameter_pack<Params...>> {
Ret invoke(request_resources& resources,
Ret (*pf)(Args ...), parameter_pack<Params...>& params) {
if constexpr (Idx == 0) return (*pf)();
else return static_cast<path_handler<
Idx - 1, Ret (*)(Args ...), parameter_pack<Params...>,
typename get_parameter<Idx - 1, Params...>::type>*
>(this)->invoke2(resources, pf, params,
get_parameter_data(resources,
get_parameter_value<Idx - 1>(params)));
}
};
template <int Idx, typename Ret, typename ...Args,
typename ...Params, typename Head, typename ...Tail>
struct path_handler<Idx, Ret (*)(Args ...),
parameter_pack<Params...>, Head, Tail...>
: path_handler<Idx + 1, Ret (*)(Args ...),
parameter_pack<Params...>, Tail...> {
template <
typename Head2, typename ...Tail2,
std::enable_if_t<sizeof...(Tail2) == sizeof...(Tail), int> = 0>
Ret invoke2(request_resources& resources,
Ret (*pf)(Args ...), parameter_pack<Params...>& params,
Head2&& head2, Tail2&& ...tail2) {
if constexpr (Idx == 0)
return (*pf)(static_cast<Head2&&>(head2),
static_cast<Tail2&&>(tail2)...);
else return static_cast<path_handler<
Idx - 1, Ret (*)(Args ...), parameter_pack<Params...>,
typename get_parameter<Idx - 1, Params...>::type, Head, Tail...>*
>(this)->invoke2(resources, pf, params,
get_parameter_data(resources,
get_parameter_value<Idx - 1>(params)),
static_cast<Head2&&>(head2), static_cast<Tail2&&>(tail2)...);
}
};
const std::vector<std::pair<std::regex, std::string>> url_regex_mapping{
{std::regex{"<int>"}, "([0-9]+)"},
{std::regex{"<str>"}, R"(([A-Za-z0-9_\.\-]+))"},
{std::regex{"<path>"}, 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> {
path_holder() = default;
virtual ~path_holder() = default;
virtual bool match(
const std::string&,
std::vector<std::string>&) const = 0;
virtual std::optional<boost::json::value> invoke(
request_resources&) = 0;
};
template <typename Func, typename Params>
class path;
template <typename Ret, typename ...Args, typename ...Params>
class path<Ret (*)(Args ...), parameter_pack<Params...>>
: public path_holder {
private:
std::regex re_;
Ret (*pf_)(Args ...);
parameter_pack<Params...> params_;
path_handler<0, Ret (*)(Args ...), parameter_pack<Params...>, Params...> handler_;
public:
path(const std::string& url, Ret (*pf)(Args ...), Params&& ...params)
: re_{get_re_url(url)}, pf_{pf},
params_{static_cast<Params&&>(params)...} {}
bool match(const std::string& url, std::vector<std::string>& 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<boost::json::value> invoke(
request_resources& resources) {
return handler_.invoke(
resources, pf_, params_);
}
};
} // router_internal
template <typename Ret, typename ...Args, typename ...Params>
std::shared_ptr<router_internal::path<Ret (*)(Args ...),
router_internal::parameter_pack<Params...>>> make_path(
const std::string& url, Ret (*pf)(Args ...), Params&& ...params) {
return std::make_shared<
router_internal::path<Ret (*)(Args ...),
router_internal::parameter_pack<Params...>>
>(url, pf, static_cast<Params&&>(params)...);
}
template <typename Ret, typename ...Args, typename ...Params>
std::shared_ptr<router_internal::path<Ret (*)(Args ...),
router_internal::parameter_pack<Params...>>> make_path(
const char* url, Ret (*pf)(Args ...), Params&& ...params) {
return std::make_shared<
router_internal::path<Ret (*)(Args ...),
router_internal::parameter_pack<Params...>>
>(url, pf, static_cast<Params&&>(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<router_internal::path_holder>;
std::vector<path_holder_type> paths_;
std::shared_ptr<server_resources> resources_;
public:
router(const std::initializer_list<path_holder_type>& paths)
: paths_{paths} {}
void set_resources(std::shared_ptr<server_resources> resources) {
resources_ = resources;
}
std::optional<boost::json::value> operator()(
asio::io_context& ioc, asio::yield_context& yield,
std::shared_ptr<websocket_session> ws_session,
const std::string& url, request_type& request, response_type& response) {
std::vector<std::string> 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

View File

@ -1,503 +0,0 @@
#include "server.hpp"
#include "logging.hpp"
#include "utils.hpp"
#include "client.hpp"
#include "websocket.hpp"
#include <iostream>
#include <string>
#include <cstddef>
#include <cstdlib>
#include <vector>
#include <optional>
#include <functional>
#include <thread>
#include <chrono>
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<http::string_body> handle_request(
http::request<http::string_body>& req, router& routes,
std::shared_ptr<websocket_session> ws_session,
asio::io_context& ioc, asio::yield_context& yield) {
const auto bad_request = [&req](beast::string_view why) {
http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<boost::json::value> 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<websocket_session_server>,
std::shared_ptr<websocket_session> session,
http::request<http::string_body>& req, router& routes,
asio::io_context& ioc, asio::yield_context yield);
class websocket_session_server
: public std::enable_shared_from_this<websocket_session_server> {
private:
friend websocket_server;
std::string address_;
std::shared_ptr<websocket_session> session_;
http::request<http::string_body> 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<http::string_body>&& 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<websocket_session_server>,
std::shared_ptr<websocket_session> session,
http::request<http::string_body>& 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 <class Send>
void handle_http_request(
std::shared_ptr<http_session>,
http::request<http::string_body> 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<http_session> {
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 <bool isRequest, class Body, class Fields>
void operator()(
http::message<isRequest, Body, Fields>&& 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<isRequest, Body, Fields>>(
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<http::string_body>> parser_;
std::shared_ptr<void> 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<websocket_session_server>(
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<send_lambda>,
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<listener> {
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<http_session>(
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<memory_session_manager>();
std::shared_ptr<server_resources> resources_ptr = std::make_shared<server_resources>();
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<listener>(
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<std::thread> 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

View File

@ -1,108 +0,0 @@
#ifndef _SESSION_HPP
#define _SESSION_HPP
#include <boost/json.hpp>
#include <cstddef>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <mutex>
#include <memory>
#include <chrono>
#include <random>
#include <limits>
#include "utils.hpp"
namespace bserv {
const std::string SESSION_NAME = "bsessionid";
// using session_type = std::map<std::string, boost::json::value>;
using session_type = boost::json::object;
struct session_manager_base
: std::enable_shared_from_this<session_manager_base> {
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_type>& 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<std::size_t> dist_;
std::map<std::string, std::size_t> str_to_int_;
std::map<std::size_t, std::string> int_to_str_;
std::map<std::size_t, std::shared_ptr<session_type>> sessions_;
// `expiry` stores <key, expiry> tuple sorted by key
std::map<std::size_t, time_point> expiry_;
// `queue` functions as a priority queue
// (the front element is the smallest)
// and stores <expiry, key> tuple sorted by
// expiry first and then key.
std::set<std::pair<time_point, std::size_t>> queue_;
mutable std::mutex lock_;
public:
memory_session_manager()
: rng_{utils::internal::get_rd_value()},
dist_{0, std::numeric_limits<std::size_t>::max()} {}
bool get_or_create(
std::string& key,
std::shared_ptr<session_type>& session_ptr) {
std::lock_guard<std::mutex> 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<session_type>();
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

View File

@ -1,246 +0,0 @@
#ifndef _UTILS_HPP
#define _UTILS_HPP
#include <cstddef>
#include <string>
#include <tuple>
#include <vector>
#include <map>
#include <random>
#include <mutex>
#include <sstream>
#include <iomanip>
#include <cryptopp/cryptlib.h>
#include <cryptopp/pwdbased.h>
#include <cryptopp/sha.h>
#include <cryptopp/base64.h>
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<std::mutex> 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<SHA256> 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::string, std::string>,
std::map<std::string, std::vector<std::string>>>
parse_params(std::string& s, std::size_t start_pos = 0, char delimiter = '&') {
std::map<std::string, std::string> dict_params;
std::map<std::string, std::vector<std::string>> 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::string,
std::map<std::string, std::string>,
std::map<std::string, std::vector<std::string>>>
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::string, std::string>{},
std::map<std::string, std::vector<std::string>>{});
auto&& [dict_params, list_params] = parse_params(s, i + 1);
return std::make_tuple(url, dict_params, list_params);
}
} // bserv::utils
#endif // _UTILS_HPP

View File

@ -1,65 +0,0 @@
#ifndef _WEBSOCKET_HPP
#define _WEBSOCKET_HPP
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <boost/json.hpp>
#include <iostream>
#include <string>
#include <cstddef>
#include <cstdlib>
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<beast::tcp_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

8
config.json Normal file
View File

@ -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"
}

58
dependencies/README.md vendored Normal file
View File

@ -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
```

1
dependencies/boost vendored Submodule

@ -0,0 +1 @@
Subproject commit 45d1a0a0ebaf0d475f420f73a8b40788192134ad

1
dependencies/cryptopp vendored Submodule

@ -0,0 +1 @@
Subproject commit 131fdc1bdf77352d7adec7e9e383efcd9e8f0075

1
dependencies/inja vendored Submodule

@ -0,0 +1 @@
Subproject commit 635e1fb183485eeb8c9a7d6ba893629cd5e00c89

1
dependencies/libpqxx vendored Submodule

@ -0,0 +1 @@
Subproject commit 819940f96d4be43ce2b7f7006e457e66047d5047

View File

@ -1,267 +0,0 @@
#ifndef _HANDLERS_HPP
#define _HANDLERS_HPP
#include <boost/json.hpp>
#include <string>
#include <memory>
#include <vector>
#include <optional>
#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<Type[i]>(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<int>("id"),
bserv::make_db_field<std::string>("username"),
bserv::make_db_field<std::string>("password"),
bserv::make_db_field<bool>("is_superuser"),
bserv::make_db_field<std::string>("first_name"),
bserv::make_db_field<std::string>("last_name"),
bserv::make_db_field<std::string>("email"),
bserv::make_db_field<bool>("is_active")
};
std::optional<boost::json::object> 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<bserv::session_type> 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<bserv::db_connection> 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<bserv::db_connection> conn,
std::shared_ptr<bserv::session_type> 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<bserv::db_connection> 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<bserv::session_type> 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<bserv::session_type> session,
std::shared_ptr<bserv::http_client> 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<bserv::session_type> session,
std::shared_ptr<bserv::websocket_server> 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

137
main.cpp
View File

@ -1,137 +0,0 @@
#include <iostream>
#include <cstdlib>
#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/<str>", &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;
}

101
templates/base.html Normal file
View File

@ -0,0 +1,101 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>WebApp - {% block title %}{% endblock %}</title>
<!-- Bootstrap core CSS -->
<link href="/statics/css/bootstrap.min.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body>
<main>
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="/">WebApp</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link {% block home_active %}{% endblock %}" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link {% block users_active %}{% endblock %}" href="/users">Users</a>
</li>
</ul>
{% if exists("user") %}
<ul class="navbar-nav mb-2 mb-md-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown07XL" data-bs-toggle="dropdown" aria-expanded="false">{{ user.username }}</a>
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end" aria-labelledby="dropdown07XL">
<li><a class="dropdown-item" href="/form_logout">Logout</a></li>
</ul>
</li>
</ul>
{% else %}
<form class="d-flex" method="post" action="/form_login">
<input class="form-control me-2" type="text" name="username" placeholder="Username" aria-label="Username">
<input class="form-control me-2" type="password" name="password" placeholder="Password" aria-label="Password">
<button class="btn btn-outline-success" type="submit">Login</button>
</form>
{% endif %}
</div>
</div>
</nav>
{% if exists("message") %}
{% if exists("success") %}
{% if success %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endif %}
{% else %}
<div class="alert alert-primary" role="alert">
{{ message }}
</div>
{% endif %}
{% endif %}
<div class="container py-4">
{% block content %}{% endblock %}
<footer class="pt-3 mt-4 text-muted border-top">
&copy; 2021
</footer>
</div>
</main>
<script src="/statics/js/bootstrap.bundle.min.js"></script>
</body>
</html>

32
templates/index.html Normal file
View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block home_active %}active{% endblock %}
{% block content %}
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">Custom jumbotron</h1>
<p class="col-md-8 fs-4">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.</p>
<button class="btn btn-primary btn-lg" type="button">Example button</button>
</div>
</div>
<div class="row align-items-md-stretch">
<div class="col-md-6">
<div class="h-100 p-5 text-white bg-dark rounded-3">
<h2>Change the background</h2>
<p>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.</p>
<button class="btn btn-outline-light" type="button">Example button</button>
</div>
</div>
<div class="col-md-6">
<div class="h-100 p-5 bg-light border rounded-3">
<h2>Add borders</h2>
<p>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.</p>
<button class="btn btn-outline-secondary" type="button">Example button</button>
</div>
</div>
</div>
{% endblock %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

121
templates/users.html Normal file
View File

@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block title %}Users{% endblock %}
{% block users_active %}active{% endblock %}
{% block content %}
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal">
Add User
</button>
<!-- Modal -->
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/form_add_user">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel">Add User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Username">
</div>
<div class="mb-3">
<label for="first_name" class="form-label">First Name</label>
<input type="text" class="form-control" id="first_name" name="first_name" placeholder="First Name">
</div>
<div class="mb-3">
<label for="last_name" class="form-label">Last Name</label>
<input type="text" class="form-control" id="last_name" name="last_name" placeholder="Last Name">
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" placeholder="name@example.com">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add</button>
</div>
</form>
</div>
</div>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">First</th>
<th scope="col">Last</th>
<th scope="col">Email</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr style="vertical-align: middle;">
<th scope="row">{{ loop.index1 }}</th>
<td>{{ user.username }}</td>
<td>{{ user.first_name }}</td>
<td>{{ user.last_name }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if exists("pagination") %}
<ul class="pagination">
{% if existsIn(pagination, "previous") %}
<li class="page-item">
<a class="page-link" href="/users/{{ pagination.previous }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% if existsIn(pagination, "left_ellipsis") %}
<li class="page-item"><a class="page-link" href="/users/1">1</a></li>
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% for page in pagination.pages_left %}
<li class="page-item"><a class="page-link" href="/users/{{ page }}">{{ page }}</a></li>
{% endfor %}
<li class="page-item active" aria-current="page"><a class="page-link" href="/users/{{ pagination.current }}">{{ pagination.current }}</a></li>
{% for page in pagination.pages_right %}
<li class="page-item"><a class="page-link" href="/users/{{ page }}">{{ page }}</a></li>
{% endfor %}
{% if existsIn(pagination, "right_ellipsis") %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
<li class="page-item"><a class="page-link" href="/users/{{ pagination.total }}">{{ pagination.total }}</a></li>
{% endif %}
{% if existsIn(pagination, "next") %}
<li class="page-item">
<a class="page-link" href="/users/{{ pagination.next }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
{% endif %}
{% endblock %}

View File

@ -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,17 +73,19 @@ 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)
# 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')

View File

@ -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,6 +35,11 @@ def test(i):
print('test failed!')
# print(f'exiting process {i}')
if __name__ == '__main__':
size_test()
# exit()
processes = [Process(target=test, args=(i, )) for i in range(P)]
print('starting')

View File

@ -10,7 +10,7 @@ from time import time
from pprint import pprint
P = 500
P = 100
def test():
@ -44,6 +44,7 @@ def test():
fun("ws://localhost:8080/echo")
)
if __name__ == '__main__':
processes = [Process(target=test) for _ in range(P)]
print('starting')