initial commit
This commit is contained in:
parent
6696de80c7
commit
a6f745a787
|
@ -1,3 +1,8 @@
|
||||||
|
test/
|
||||||
|
build/*
|
||||||
|
!build/README.md
|
||||||
|
.*
|
||||||
|
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
*.d
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
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_executable(bserv bserv.cpp)
|
||||||
|
target_link_libraries(bserv
|
||||||
|
pthread
|
||||||
|
boost_thread
|
||||||
|
boost_log
|
||||||
|
boost_log_setup
|
||||||
|
pqxx
|
||||||
|
pq
|
||||||
|
cryptopp)
|
69
README.md
69
README.md
|
@ -1 +1,70 @@
|
||||||
# bserv
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
You can import the sample database:
|
||||||
|
|
||||||
|
- Create the database in `psql`:
|
||||||
|
```
|
||||||
|
create database bserv;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Create the table in the `shell` using a sample script:
|
||||||
|
```
|
||||||
|
psql bserv < db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
|
||||||
|
Configure routing in [routing.hpp](routing.hpp).
|
||||||
|
|
||||||
|
|
||||||
|
### Handlers
|
||||||
|
|
||||||
|
Write the handlers in [handlers.hpp](handlers.hpp)
|
||||||
|
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Please refer to [this](build/README.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Run in `shell`:
|
||||||
|
```
|
||||||
|
./build/bserv
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
This test is performed by Jmeter.
|
||||||
|
|
||||||
|
The unit for throughput is Transaction per second.
|
||||||
|
|
||||||
|
|
||||||
|
|URL|bserv|Java Spring Boot|
|
||||||
|
|:-:|:-:|:-:|
|
||||||
|
|`/login`|139.55||
|
||||||
|
|`/find/<user>`|958.77||
|
||||||
|
|
||||||
|
|
||||||
|
### Computer Hardware:
|
||||||
|
- Intel Core i9-9900K x 4
|
||||||
|
- 16GB RAM
|
||||||
|
|
|
@ -0,0 +1,374 @@
|
||||||
|
/**
|
||||||
|
* bserv - Boost-based HTTP Server
|
||||||
|
*
|
||||||
|
* reference:
|
||||||
|
* https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/server/async/http_server_async.cpp
|
||||||
|
* https://www.boost.org/doc/libs/1_75_0/libs/beast/example/advanced/server/advanced_server.cpp
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <boost/beast.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/optional.hpp>
|
||||||
|
#include <boost/json/src.hpp>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include "utils.hpp"
|
||||||
|
#include "routing.hpp"
|
||||||
|
#include "database.hpp"
|
||||||
|
|
||||||
|
namespace bserv {
|
||||||
|
|
||||||
|
namespace beast = boost::beast;
|
||||||
|
namespace http = beast::http;
|
||||||
|
namespace asio = boost::asio;
|
||||||
|
namespace json = boost::json;
|
||||||
|
using asio::ip::tcp;
|
||||||
|
|
||||||
|
void fail(const beast::error_code& ec, const char* what) {
|
||||||
|
lgerror << what << ": " << ec.message() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 Body, class Allocator, class Send>
|
||||||
|
void handle_request(
|
||||||
|
http::request<Body, http::basic_fields<Allocator>>&& req,
|
||||||
|
Send&& send) {
|
||||||
|
|
||||||
|
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(std::string{url}, req, res);
|
||||||
|
} catch (const url_not_found_exception& e) {
|
||||||
|
send(not_found(url));
|
||||||
|
return;
|
||||||
|
} catch (const bad_request_exception& e) {
|
||||||
|
send(bad_request("Request body is not a valid JSON string."));
|
||||||
|
return;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
send(server_error(e.what()));
|
||||||
|
return;
|
||||||
|
} catch (...) {
|
||||||
|
send(server_error("Unknown exception."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.has_value()) {
|
||||||
|
res.body() = json::serialize(val.value());
|
||||||
|
res.prepare_payload();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(std::move(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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_;
|
||||||
|
beast::tcp_stream stream_;
|
||||||
|
beast::flat_buffer buffer_;
|
||||||
|
boost::optional<
|
||||||
|
http::request_parser<http::string_body>> parser_;
|
||||||
|
std::shared_ptr<void> res_;
|
||||||
|
// 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(30));
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
// handles the request and sends the response
|
||||||
|
handle_request(parser_->release(), lambda_);
|
||||||
|
// 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(tcp::socket&& socket)
|
||||||
|
: lambda_{*this}, stream_{std::move(socket)} /*,
|
||||||
|
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_;
|
||||||
|
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>(
|
||||||
|
std::move(socket))->run();
|
||||||
|
}
|
||||||
|
do_accept();
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
listener(
|
||||||
|
asio::io_context& ioc,
|
||||||
|
tcp::endpoint endpoint)
|
||||||
|
: ioc_{ioc},
|
||||||
|
acceptor_{asio::make_strand(ioc)} {
|
||||||
|
beast::error_code ec;
|
||||||
|
acceptor_.open(endpoint.protocol(), ec);
|
||||||
|
if (ec) {
|
||||||
|
fail(ec, "listener::acceptor open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acceptor_.set_option(
|
||||||
|
asio::socket_base::reuse_address(true), ec);
|
||||||
|
if (ec) {
|
||||||
|
fail(ec, "listener::acceptor set_option");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acceptor_.bind(endpoint, ec);
|
||||||
|
if (ec) {
|
||||||
|
fail(ec, "listener::acceptor bind");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acceptor_.listen(
|
||||||
|
asio::socket_base::max_listen_connections, ec);
|
||||||
|
if (ec) {
|
||||||
|
fail(ec, "listener::acceptor listen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void run() {
|
||||||
|
asio::dispatch(
|
||||||
|
acceptor_.get_executor(),
|
||||||
|
beast::bind_front_handler(
|
||||||
|
&listener::do_accept,
|
||||||
|
shared_from_this()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void show_config() {
|
||||||
|
lginfo << NAME << " config:"
|
||||||
|
<< "\nport: " << PORT
|
||||||
|
<< "\nthreads: " << NUM_THREADS
|
||||||
|
<< "\ndb-conn: " << NUM_DB_CONN
|
||||||
|
<< "\npayload: " << PAYLOAD_LIMIT / 1024 / 1024
|
||||||
|
<< "\nrotation: " << LOG_ROTATION_SIZE / 1024 / 1024
|
||||||
|
<< "\nlog path: " << LOG_PATH
|
||||||
|
<< "\nconn-str: " << DB_CONN_STR << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // bserv
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
using namespace bserv;
|
||||||
|
if (parse_arguments(argc, argv))
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
init_logging();
|
||||||
|
show_config();
|
||||||
|
|
||||||
|
// some initializations must be done after parsing the arguments
|
||||||
|
// e.g. database connection
|
||||||
|
try {
|
||||||
|
db_conn_mgr = std::make_shared<
|
||||||
|
db_connection_manager>(DB_CONN_STR, NUM_DB_CONN);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
lgfatal << "db connection initialization failed: " << e.what() << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
session_mgr = std::make_shared<memory_session>();
|
||||||
|
|
||||||
|
// io_context for all I/O
|
||||||
|
asio::io_context ioc{NUM_THREADS};
|
||||||
|
|
||||||
|
// creates and launches a listening port
|
||||||
|
std::make_shared<listener>(
|
||||||
|
ioc, tcp::endpoint{tcp::v4(), PORT})->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 << NAME << " started";
|
||||||
|
|
||||||
|
// runs the I/O service on the requested number of threads
|
||||||
|
std::vector<std::thread> v;
|
||||||
|
v.reserve(NUM_THREADS - 1);
|
||||||
|
for (int i = 1; i < NUM_THREADS; ++i)
|
||||||
|
v.emplace_back([&]{ ioc.run(); });
|
||||||
|
ioc.run();
|
||||||
|
|
||||||
|
// if we get here, it means we got a SIGINT or SIGTERM
|
||||||
|
lginfo << "exiting " << NAME;
|
||||||
|
|
||||||
|
// blocks until all the threads exit
|
||||||
|
for (auto & t : v) t.join();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Build
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake ..
|
||||||
|
cmake --build .
|
||||||
|
```
|
|
@ -0,0 +1,10 @@
|
||||||
|
#ifndef _COMMON_HPP
|
||||||
|
#define _COMMON_HPP
|
||||||
|
|
||||||
|
#include "database.hpp"
|
||||||
|
#include "session.hpp"
|
||||||
|
#include "router.hpp"
|
||||||
|
#include "utils.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
|
||||||
|
#endif // _COMMON_HPP
|
|
@ -0,0 +1,115 @@
|
||||||
|
#ifndef _CONFIG_HPP
|
||||||
|
#define _CONFIG_HPP
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace bserv {
|
||||||
|
|
||||||
|
const char* NAME = "bserv";
|
||||||
|
|
||||||
|
unsigned short PORT = 8080;
|
||||||
|
int NUM_THREADS = 4;
|
||||||
|
int NUM_DB_CONN = 10;
|
||||||
|
|
||||||
|
std::size_t PAYLOAD_LIMIT = 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
std::size_t LOG_ROTATION_SIZE = 4 * 1024 * 1024;
|
||||||
|
std::string LOG_PATH = "./log/";
|
||||||
|
std::string DB_CONN_STR = "dbname=bserv";
|
||||||
|
|
||||||
|
void show_usage() {
|
||||||
|
std::cout << "Usage: " << NAME << " [OPTION...]\n"
|
||||||
|
<< NAME << " is a C++ Boost-based HTTP server.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
<< " " << 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: 4)\n"
|
||||||
|
" --num-conn number of database connections (default: 10)\n"
|
||||||
|
" --payload payload limit for request in mega bytes (default: 1)\n"
|
||||||
|
" --rotation log rotation size in mega bytes (default: 4)\n"
|
||||||
|
" --log-path log path (default: ./log/)\n"
|
||||||
|
" -c, --conn-str connection string (default: dbname=bserv)"
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns `true` if error occurs
|
||||||
|
bool parse_arguments(int argc, char* argv[]) {
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||||
|
show_usage();
|
||||||
|
return true;
|
||||||
|
} else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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], "--payload") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
PAYLOAD_LIMIT = atoi(argv[i + 1]) * 1024 * 1024;
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Missing value after: " << argv[i] << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--rotation") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
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) {
|
||||||
|
LOG_PATH = argv[i + 1];
|
||||||
|
if (LOG_PATH.back() != '/')
|
||||||
|
LOG_PATH += '/';
|
||||||
|
++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) {
|
||||||
|
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();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // bserv
|
||||||
|
|
||||||
|
#endif // _CONFIG_HPP
|
|
@ -0,0 +1,303 @@
|
||||||
|
#ifndef _DATABASE_HPP
|
||||||
|
#define _DATABASE_HPP
|
||||||
|
|
||||||
|
#include <boost/json/src.hpp>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <queue>
|
||||||
|
#include <optional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <memory>
|
||||||
|
#include <initializer_list>
|
||||||
|
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
|
||||||
|
namespace bserv {
|
||||||
|
|
||||||
|
class db_connection_manager;
|
||||||
|
|
||||||
|
class db_connection {
|
||||||
|
private:
|
||||||
|
db_connection_manager& mgr_;
|
||||||
|
std::shared_ptr<pqxx::connection> conn_;
|
||||||
|
public:
|
||||||
|
db_connection(
|
||||||
|
db_connection_manager& mgr,
|
||||||
|
std::shared_ptr<pqxx::connection> 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();
|
||||||
|
pqxx::connection& get() { return *conn_; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// provides the database connection pool functionality
|
||||||
|
class db_connection_manager {
|
||||||
|
private:
|
||||||
|
std::queue<std::shared_ptr<pqxx::connection>> 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<pqxx::connection>(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<pqxx::connection> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<db_connection_manager> db_conn_mgr;
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class db_parameter {
|
||||||
|
public:
|
||||||
|
virtual ~db_parameter() = default;
|
||||||
|
virtual std::string get_value(pqxx::work&) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class db_name : public db_parameter {
|
||||||
|
private:
|
||||||
|
std::string value_;
|
||||||
|
public:
|
||||||
|
db_name(const std::string& value)
|
||||||
|
: value_{value} {}
|
||||||
|
std::string get_value(pqxx::work& w) {
|
||||||
|
return w.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(pqxx::work&) {
|
||||||
|
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(pqxx::work& w) {
|
||||||
|
return w.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(pqxx::work&) {
|
||||||
|
return value_ ? "true" : "false";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<db_parameter> convert_parameter(
|
||||||
|
const char* param) {
|
||||||
|
return std::make_shared<db_value<std::string>>(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
pqxx::work& w, std::shared_ptr<Params>... params) {
|
||||||
|
return {params->get_value(w)...};
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************
|
||||||
|
|
||||||
|
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 pqxx::row& row, 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 pqxx::row& row, 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 pqxx::row& row, 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 pqxx::row& row) {
|
||||||
|
boost::json::object obj;
|
||||||
|
for (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 pqxx::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 pqxx::result& result) {
|
||||||
|
if (result.size() == 0) return std::nullopt;
|
||||||
|
if (result.size() == 1) return convert_row(result[0]);
|
||||||
|
// result.size() > 1
|
||||||
|
throw invalid_operation_exception{
|
||||||
|
"too many objects to convert"};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
// db_exec(tx, "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?'
|
||||||
|
// ======================================================================================
|
||||||
|
// db_exec(tx, "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>
|
||||||
|
pqxx::result db_exec(pqxx::work& w,
|
||||||
|
const std::string& s, const Params&... params) {
|
||||||
|
std::vector<std::string> param_vec =
|
||||||
|
db_internal::convert_parameters(
|
||||||
|
w, db_internal::convert_parameter(params)...);
|
||||||
|
size_t idx = 0;
|
||||||
|
std::string query;
|
||||||
|
for (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 w.exec(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: add support for time conversions between postgresql and c++, use timestamp?
|
||||||
|
// what about time zone?
|
||||||
|
|
||||||
|
} // bserv
|
||||||
|
|
||||||
|
#endif // _DATABASE_HPP
|
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE auth_user (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
username character varying(255) NOT NULL UNIQUE,
|
||||||
|
password character varying(255) NOT NULL,
|
||||||
|
is_superuser boolean NOT NULL,
|
||||||
|
first_name character varying(255) NOT NULL,
|
||||||
|
last_name character varying(255) NOT NULL,
|
||||||
|
email character varying(255) NOT NULL,
|
||||||
|
is_active boolean NOT NULL
|
||||||
|
);
|
|
@ -0,0 +1,212 @@
|
||||||
|
#ifndef _HANDLERS_HPP
|
||||||
|
#define _HANDLERS_HPP
|
||||||
|
|
||||||
|
#include <boost/json/src.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
|
||||||
|
#include "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(
|
||||||
|
pqxx::work& tx,
|
||||||
|
const std::string& username) {
|
||||||
|
pqxx::result r = bserv::db_exec(tx,
|
||||||
|
"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")) {
|
||||||
|
auto& user = session["user"].as_object();
|
||||||
|
obj = {
|
||||||
|
{"msg", std::string{"welcome, "}
|
||||||
|
+ user["username"].as_string().c_str() + "!"}
|
||||||
|
};
|
||||||
|
} 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();
|
||||||
|
pqxx::work tx{conn->get()};
|
||||||
|
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();
|
||||||
|
pqxx::result r = bserv::db_exec(tx,
|
||||||
|
"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();
|
||||||
|
pqxx::work tx{conn->get()};
|
||||||
|
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) {
|
||||||
|
pqxx::work tx{conn->get()};
|
||||||
|
auto user = get_user(tx, username);
|
||||||
|
if (!user.has_value()) {
|
||||||
|
return {
|
||||||
|
{"success", false},
|
||||||
|
{"message", "requested user does not exist"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _HANDLERS_HPP
|
|
@ -0,0 +1,42 @@
|
||||||
|
#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 "config.hpp"
|
||||||
|
|
||||||
|
namespace bserv {
|
||||||
|
|
||||||
|
namespace logging = boost::log;
|
||||||
|
namespace keywords = boost::log::keywords;
|
||||||
|
namespace src = boost::log::sources;
|
||||||
|
|
||||||
|
// this function should be called in `main`
|
||||||
|
// right after the configurations are loaded.
|
||||||
|
void init_logging() {
|
||||||
|
logging::add_file_log(
|
||||||
|
keywords::file_name = LOG_PATH + NAME + "_%Y%m%d_%H-%M-%S.%N.log",
|
||||||
|
keywords::rotation_size = 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)
|
||||||
|
|
||||||
|
} // bserv
|
||||||
|
|
||||||
|
#endif // _LOGGING_HPP
|
|
@ -0,0 +1,354 @@
|
||||||
|
#ifndef _ROUTER_HPP
|
||||||
|
#define _ROUTER_HPP
|
||||||
|
|
||||||
|
#include <boost/beast.hpp>
|
||||||
|
#include <boost/json/src.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <regex>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
|
||||||
|
#include "database.hpp"
|
||||||
|
#include "session.hpp"
|
||||||
|
#include "utils.hpp"
|
||||||
|
#include "config.hpp"
|
||||||
|
|
||||||
|
namespace bserv {
|
||||||
|
|
||||||
|
namespace beast = boost::beast;
|
||||||
|
namespace http = beast::http;
|
||||||
|
|
||||||
|
using request_type = http::request<http::string_body>;
|
||||||
|
using response_type = http::response<http::string_body>;
|
||||||
|
|
||||||
|
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> transaction;
|
||||||
|
|
||||||
|
} // 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(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type&, response_type&, Type&& val) {
|
||||||
|
return static_cast<Type&&>(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <int N, std::enable_if_t<(N >= 0), int> = 0>
|
||||||
|
const std::string& get_parameter_data(
|
||||||
|
const std::vector<std::string>& url_params,
|
||||||
|
request_type&, response_type&,
|
||||||
|
placeholders::placeholder<N>) {
|
||||||
|
return url_params[N];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<session_type> get_parameter_data(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type& request, response_type& response,
|
||||||
|
placeholders::placeholder<-1>) {
|
||||||
|
std::string cookie_str{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 (session_mgr->get_or_create(session_id, session_ptr)) {
|
||||||
|
response.set(http::field::set_cookie, SESSION_NAME + "=" + session_id);
|
||||||
|
}
|
||||||
|
return session_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
request_type& get_parameter_data(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type& request, response_type&,
|
||||||
|
placeholders::placeholder<-2>) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
response_type& get_parameter_data(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type&, response_type& response,
|
||||||
|
placeholders::placeholder<-3>) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::json::object get_parameter_data(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type& request, response_type&,
|
||||||
|
placeholders::placeholder<-4>) {
|
||||||
|
std::string target{request.target()};
|
||||||
|
auto&& [url, dict_params, list_params] = utils::parse_url(target);
|
||||||
|
boost::ignore_unused(url);
|
||||||
|
boost::json::object body;
|
||||||
|
if (!request.body().empty()) {
|
||||||
|
try {
|
||||||
|
body = boost::json::parse(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<db_connection> get_parameter_data(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type&, response_type&,
|
||||||
|
placeholders::placeholder<-5>) {
|
||||||
|
return db_conn_mgr->get_or_block();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(Ret (*pf)(Args ...), parameter_pack<Params...>& params,
|
||||||
|
const std::vector<std::string>& url_params,
|
||||||
|
request_type& request, response_type& response) {
|
||||||
|
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(pf, params, url_params, request, response,
|
||||||
|
get_parameter_data(url_params, request, response,
|
||||||
|
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(Ret (*pf)(Args ...), parameter_pack<Params...>& params,
|
||||||
|
const std::vector<std::string>& url_params,
|
||||||
|
request_type& request, response_type& response,
|
||||||
|
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(pf, params, url_params, request, response,
|
||||||
|
get_parameter_data(url_params, request, response,
|
||||||
|
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_/\.\-]+))"}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
const std::vector<std::string>&,
|
||||||
|
request_type&, response_type&) = 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(
|
||||||
|
const std::vector<std::string>& url_params,
|
||||||
|
request_type& request, response_type& response) {
|
||||||
|
return handler_.invoke(
|
||||||
|
pf_, params_, url_params,
|
||||||
|
request, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // 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_;
|
||||||
|
public:
|
||||||
|
router(const std::initializer_list<path_holder_type>& paths)
|
||||||
|
: paths_{paths} {}
|
||||||
|
std::optional<boost::json::value> operator()(
|
||||||
|
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))
|
||||||
|
return ptr->invoke(url_params, request, response);
|
||||||
|
}
|
||||||
|
throw url_not_found_exception{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // bserv
|
||||||
|
|
||||||
|
#endif // _ROUTER_HPP
|
|
@ -0,0 +1,32 @@
|
||||||
|
#ifndef _ROUTING_HPP
|
||||||
|
#define _ROUTING_HPP
|
||||||
|
|
||||||
|
#include "router.hpp"
|
||||||
|
|
||||||
|
#include "handlers.hpp"
|
||||||
|
|
||||||
|
namespace bserv {
|
||||||
|
|
||||||
|
bserv::router routes{
|
||||||
|
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::transaction),
|
||||||
|
bserv::make_path("/login", &user_login,
|
||||||
|
bserv::placeholders::request,
|
||||||
|
bserv::placeholders::json_params,
|
||||||
|
bserv::placeholders::transaction,
|
||||||
|
bserv::placeholders::session),
|
||||||
|
bserv::make_path("/logout", &user_logout,
|
||||||
|
bserv::placeholders::session),
|
||||||
|
bserv::make_path("/find/<str>", &find_user,
|
||||||
|
bserv::placeholders::transaction,
|
||||||
|
bserv::placeholders::_1)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // bserv
|
||||||
|
|
||||||
|
#endif // _ROUTING_HPP
|
|
@ -0,0 +1,110 @@
|
||||||
|
#ifndef _SESSION_HPP
|
||||||
|
#define _SESSION_HPP
|
||||||
|
|
||||||
|
#include <boost/json/src.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_base
|
||||||
|
: std::enable_shared_from_this<session_base> {
|
||||||
|
virtual ~session_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;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<session_base> session_mgr;
|
||||||
|
|
||||||
|
class memory_session : public session_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()
|
||||||
|
: 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
|
|
@ -0,0 +1,224 @@
|
||||||
|
#ifndef _UTILS_HPP
|
||||||
|
#define _UTILS_HPP
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <random>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
} // 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 = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = '&') {
|
||||||
|
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!
|
||||||
|
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
|
Loading…
Reference in New Issue