initial commit

This commit is contained in:
jie 2021-03-05 15:39:47 +08:00
parent 6696de80c7
commit a6f745a787
15 changed files with 1891 additions and 1 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
test/
build/*
!build/README.md
.*
# Prerequisites # Prerequisites
*.d *.d

24
CMakeLists.txt Normal file
View File

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

View File

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

374
bserv.cpp Normal file
View File

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

6
build/README.md Normal file
View File

@ -0,0 +1,6 @@
# Build
```
cmake ..
cmake --build .
```

10
common.hpp Normal file
View File

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

115
config.hpp Normal file
View File

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

303
database.hpp Normal file
View File

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

10
db.sql Normal file
View File

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

212
handlers.hpp Normal file
View File

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

42
logging.hpp Normal file
View File

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

354
router.hpp Normal file
View File

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

32
routing.hpp Normal file
View File

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

110
session.hpp Normal file
View File

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

224
utils.hpp Normal file
View File

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