mirror of
https://github.com/boostorg/beast.git
synced 2025-08-03 06:44:39 +02:00
Add detail/prng.hpp
This commit is contained in:
@@ -2,6 +2,7 @@ Version 205
|
||||
|
||||
* Doc work
|
||||
* Add detail/soft_mutex.hpp
|
||||
* Add detail/prng.hpp
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
@@ -36,11 +36,11 @@ inline
|
||||
void
|
||||
make_sec_ws_key(sec_ws_key_type& key)
|
||||
{
|
||||
auto p = stream_prng::prng();
|
||||
auto g = make_prng(true);
|
||||
char a[16];
|
||||
for(int i = 0; i < 16; i += 4)
|
||||
{
|
||||
auto const v = p->secure();
|
||||
auto const v = g();
|
||||
a[i ] = v & 0xff;
|
||||
a[i+1] = (v >> 8) & 0xff;
|
||||
a[i+2] = (v >> 16) & 0xff;
|
||||
|
315
include/boost/beast/websocket/detail/impl/prng.ipp
Normal file
315
include/boost/beast/websocket/detail/impl/prng.ipp
Normal file
@@ -0,0 +1,315 @@
|
||||
//
|
||||
// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
// Official repository: https://github.com/boostorg/beast
|
||||
//
|
||||
|
||||
#ifndef BOOST_BEAST_WEBSOCKET_DETAIL_IMPL_PRNG_IPP
|
||||
#define BOOST_BEAST_WEBSOCKET_DETAIL_IMPL_PRNG_IPP
|
||||
|
||||
#include <boost/beast/core/detail/chacha.hpp>
|
||||
#include <boost/align/aligned_alloc.hpp>
|
||||
#include <boost/throw_exception.hpp>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace boost {
|
||||
namespace beast {
|
||||
namespace websocket {
|
||||
namespace detail {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::uint32_t const*
|
||||
prng_seed(std::seed_seq* ss)
|
||||
{
|
||||
struct data
|
||||
{
|
||||
std::uint32_t v[8];
|
||||
|
||||
explicit
|
||||
data(std::seed_seq* pss)
|
||||
{
|
||||
if(! pss)
|
||||
{
|
||||
std::random_device g;
|
||||
std::seed_seq ss{
|
||||
g(), g(), g(), g(),
|
||||
g(), g(), g(), g()};
|
||||
ss.generate(v, v+8);
|
||||
}
|
||||
else
|
||||
{
|
||||
pss->generate(v, v+8);
|
||||
}
|
||||
}
|
||||
};
|
||||
static data const d(ss);
|
||||
return d.v;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
template<class T>
|
||||
class prng_pool
|
||||
{
|
||||
std::mutex m_;
|
||||
T* head_ = nullptr;
|
||||
|
||||
public:
|
||||
static
|
||||
prng_pool&
|
||||
instance()
|
||||
{
|
||||
static prng_pool p;
|
||||
return p;
|
||||
}
|
||||
|
||||
~prng_pool()
|
||||
{
|
||||
for(auto p = head_; p;)
|
||||
{
|
||||
auto next = p->next;
|
||||
p->~T();
|
||||
boost::alignment::aligned_free(p);
|
||||
p = next;
|
||||
}
|
||||
}
|
||||
|
||||
prng::ref
|
||||
acquire()
|
||||
{
|
||||
{
|
||||
std::lock_guard<
|
||||
std::mutex> g(m_);
|
||||
if(head_)
|
||||
{
|
||||
auto p = head_;
|
||||
head_ = head_->next;
|
||||
return prng::ref(*p);
|
||||
}
|
||||
}
|
||||
auto p = boost::alignment::aligned_alloc(
|
||||
16, sizeof(T));
|
||||
if(! p)
|
||||
BOOST_THROW_EXCEPTION(std::bad_alloc{});
|
||||
return prng::ref(*(::new(p) T()));
|
||||
}
|
||||
|
||||
void
|
||||
release(T& t)
|
||||
{
|
||||
std::lock_guard<
|
||||
std::mutex> g(m_);
|
||||
t.next = head_;
|
||||
head_ = &t;
|
||||
}
|
||||
};
|
||||
|
||||
prng::ref
|
||||
make_prng_no_tls(bool secure)
|
||||
{
|
||||
class fast_prng final : public prng
|
||||
{
|
||||
int refs_ = 0;
|
||||
std::minstd_rand r_;
|
||||
|
||||
public:
|
||||
fast_prng* next;
|
||||
|
||||
fast_prng()
|
||||
: r_([]
|
||||
{
|
||||
static std::atomic<
|
||||
std::uint64_t> nonce{0};
|
||||
auto const pv = prng_seed();
|
||||
return static_cast<value_type>(
|
||||
pv[0] + pv[1] + pv[2] + pv[3] +
|
||||
pv[4] + pv[5] + pv[6] + pv[7] +
|
||||
++nonce);
|
||||
}())
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
prng&
|
||||
acquire() noexcept override
|
||||
{
|
||||
++refs_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void
|
||||
release() noexcept override
|
||||
{
|
||||
if(--refs_ == 0)
|
||||
prng_pool<fast_prng>::instance().release(*this);
|
||||
}
|
||||
|
||||
value_type
|
||||
operator()() noexcept override
|
||||
{
|
||||
return r_();
|
||||
}
|
||||
};
|
||||
|
||||
class secure_prng final : public prng
|
||||
{
|
||||
int refs_ = 0;
|
||||
beast::detail::chacha<20> r_;
|
||||
|
||||
public:
|
||||
secure_prng* next;
|
||||
|
||||
secure_prng()
|
||||
: r_(prng_seed(), []
|
||||
{
|
||||
static std::atomic<
|
||||
std::uint64_t> nonce{0};
|
||||
return ++nonce;
|
||||
}())
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
prng&
|
||||
acquire() noexcept override
|
||||
{
|
||||
++refs_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void
|
||||
release() noexcept override
|
||||
{
|
||||
if(--refs_ == 0)
|
||||
prng_pool<secure_prng>::instance().release(*this);
|
||||
}
|
||||
|
||||
value_type
|
||||
operator()() noexcept override
|
||||
{
|
||||
return r_();
|
||||
}
|
||||
};
|
||||
|
||||
if(secure)
|
||||
return prng_pool<secure_prng>::instance().acquire();
|
||||
|
||||
return prng_pool<fast_prng>::instance().acquire();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#if ! BOOST_BEAST_NO_THREAD_LOCAL
|
||||
prng::ref
|
||||
make_prng_tls(bool secure)
|
||||
{
|
||||
class fast_prng final : public prng
|
||||
{
|
||||
std::minstd_rand r_;
|
||||
|
||||
public:
|
||||
fast_prng()
|
||||
: r_([]
|
||||
{
|
||||
static std::atomic<
|
||||
std::uint64_t> nonce{0};
|
||||
auto const pv = prng_seed();
|
||||
return static_cast<value_type>(
|
||||
pv[0] + pv[1] + pv[2] + pv[3] +
|
||||
pv[4] + pv[5] + pv[6] + pv[7] +
|
||||
++nonce);
|
||||
}())
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
prng&
|
||||
acquire() noexcept override
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
void
|
||||
release() noexcept override
|
||||
{
|
||||
}
|
||||
|
||||
value_type
|
||||
operator()() noexcept override
|
||||
{
|
||||
return r_();
|
||||
}
|
||||
};
|
||||
|
||||
class secure_prng final : public prng
|
||||
{
|
||||
beast::detail::chacha<20> r_;
|
||||
|
||||
public:
|
||||
secure_prng()
|
||||
: r_(prng_seed(), []
|
||||
{
|
||||
static std::atomic<
|
||||
std::uint64_t> nonce{0};
|
||||
return ++nonce;
|
||||
}())
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
prng&
|
||||
acquire() noexcept override
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
void
|
||||
release() noexcept override
|
||||
{
|
||||
}
|
||||
|
||||
value_type
|
||||
operator()() noexcept override
|
||||
{
|
||||
return r_();
|
||||
}
|
||||
};
|
||||
|
||||
if(secure)
|
||||
{
|
||||
thread_local secure_prng sp;
|
||||
return prng::ref(sp);
|
||||
}
|
||||
|
||||
thread_local fast_prng fp;
|
||||
return prng::ref(fp);
|
||||
}
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
prng::ref
|
||||
make_prng(bool secure)
|
||||
{
|
||||
#if BOOST_BEAST_NO_THREAD_LOCAL
|
||||
return make_prng_no_tls(secure);
|
||||
#else
|
||||
return make_prng_tls(secure);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // detail
|
||||
} // websocket
|
||||
} // beast
|
||||
} // boost
|
||||
|
||||
#endif
|
138
include/boost/beast/websocket/detail/prng.hpp
Normal file
138
include/boost/beast/websocket/detail/prng.hpp
Normal file
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
// Official repository: https://github.com/boostorg/beast
|
||||
//
|
||||
|
||||
#ifndef BOOST_BEAST_WEBSOCKET_DETAIL_PRNG_HPP
|
||||
#define BOOST_BEAST_WEBSOCKET_DETAIL_PRNG_HPP
|
||||
|
||||
#include <boost/beast/core/detail/config.hpp>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
namespace boost {
|
||||
namespace beast {
|
||||
namespace websocket {
|
||||
namespace detail {
|
||||
|
||||
// Type-erased UniformRandomBitGenerator
|
||||
// with 32-bit unsigned integer value_type
|
||||
//
|
||||
class prng
|
||||
{
|
||||
protected:
|
||||
virtual ~prng() = default;
|
||||
virtual prng& acquire() noexcept = 0;
|
||||
virtual void release() noexcept = 0;
|
||||
virtual std::uint32_t operator()() noexcept = 0;
|
||||
|
||||
public:
|
||||
using value_type = std::uint32_t;
|
||||
|
||||
class ref
|
||||
{
|
||||
prng& p_;
|
||||
|
||||
public:
|
||||
ref& operator=(ref const&) = delete;
|
||||
|
||||
~ref()
|
||||
{
|
||||
p_.release();
|
||||
}
|
||||
|
||||
explicit
|
||||
ref(prng& p) noexcept
|
||||
: p_(p.acquire())
|
||||
{
|
||||
}
|
||||
|
||||
ref(ref const& other) noexcept
|
||||
: p_(other.p_.acquire())
|
||||
{
|
||||
}
|
||||
|
||||
value_type
|
||||
operator()() noexcept
|
||||
{
|
||||
return p_();
|
||||
}
|
||||
|
||||
static
|
||||
value_type constexpr
|
||||
min() noexcept
|
||||
{
|
||||
return (std::numeric_limits<
|
||||
value_type>::min)();
|
||||
}
|
||||
|
||||
static
|
||||
value_type constexpr
|
||||
max() noexcept
|
||||
{
|
||||
return (std::numeric_limits<
|
||||
value_type>::max)();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Manually seed the prngs, must be called
|
||||
// before acquiring a prng for the first time.
|
||||
//
|
||||
BOOST_BEAST_DECL
|
||||
std::uint32_t const*
|
||||
prng_seed(std::seed_seq* ss = nullptr);
|
||||
|
||||
// Acquire a PRNG using the no-TLS implementation
|
||||
//
|
||||
BOOST_BEAST_DECL
|
||||
prng::ref
|
||||
make_prng_no_tls(bool secure);
|
||||
|
||||
// Acquire a PRNG using the TLS implementation
|
||||
//
|
||||
#if ! BOOST_BEAST_NO_THREAD_LOCAL
|
||||
BOOST_BEAST_DECL
|
||||
prng::ref
|
||||
make_prng_tls(bool secure);
|
||||
#endif
|
||||
|
||||
// Acquire a PRNG using the TLS implementation if it
|
||||
// is available, otherwise using the no-TLS implementation.
|
||||
//
|
||||
BOOST_BEAST_DECL
|
||||
prng::ref
|
||||
make_prng(bool secure);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
struct stream_prng
|
||||
{
|
||||
bool secure_prng_ = true;
|
||||
|
||||
std::uint32_t
|
||||
create_mask()
|
||||
{
|
||||
auto g = make_prng(secure_prng_);
|
||||
for(;;)
|
||||
if(auto key = g())
|
||||
return key;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // detail
|
||||
} // websocket
|
||||
} // beast
|
||||
} // boost
|
||||
|
||||
#include <boost/beast/websocket/detail/impl/prng.ipp>
|
||||
|
||||
#endif
|
@@ -15,210 +15,21 @@
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/websocket/option.hpp>
|
||||
#include <boost/beast/websocket/detail/pmd_extension.hpp>
|
||||
#include <boost/beast/websocket/detail/prng.hpp>
|
||||
#include <boost/beast/zlib/deflate_stream.hpp>
|
||||
#include <boost/beast/zlib/inflate_stream.hpp>
|
||||
#include <boost/beast/core/buffers_suffix.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/detail/chacha.hpp>
|
||||
#include <boost/beast/core/detail/clamp.hpp>
|
||||
#include <boost/align/aligned_alloc.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/core/exchange.hpp>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <random>
|
||||
|
||||
// Turn this on to avoid using thread_local
|
||||
//#define BOOST_BEAST_NO_THREAD_LOCAL 1
|
||||
|
||||
#ifdef BOOST_BEAST_NO_THREAD_LOCAL
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#endif
|
||||
|
||||
namespace boost {
|
||||
namespace beast {
|
||||
namespace websocket {
|
||||
namespace detail {
|
||||
|
||||
struct stream_prng
|
||||
{
|
||||
bool secure_prng_ = true;
|
||||
|
||||
struct prng_type
|
||||
{
|
||||
std::minstd_rand fast;
|
||||
beast::detail::chacha<20> secure;
|
||||
|
||||
#if BOOST_BEAST_NO_THREAD_LOCAL
|
||||
prng_type* next = nullptr;
|
||||
#endif
|
||||
|
||||
prng_type(std::uint32_t const* v, std::uint64_t stream)
|
||||
: fast(static_cast<typename decltype(fast)::result_type>(
|
||||
v[0] + v[1] + v[2] + v[3] + v[4] + v[5] + v[6] + v[7] + stream))
|
||||
, secure(v, stream)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class prng_ref
|
||||
{
|
||||
prng_type* p_;
|
||||
|
||||
public:
|
||||
prng_ref& operator=(prng_ref&&) = delete;
|
||||
|
||||
explicit
|
||||
prng_ref(prng_type& p)
|
||||
: p_(&p)
|
||||
{
|
||||
}
|
||||
|
||||
prng_ref(prng_ref&& other)
|
||||
: p_(boost::exchange(other.p_, nullptr))
|
||||
{
|
||||
}
|
||||
|
||||
#ifdef BOOST_BEAST_NO_THREAD_LOCAL
|
||||
~prng_ref()
|
||||
{
|
||||
if(p_)
|
||||
pool::impl().release(*p_);
|
||||
}
|
||||
#endif
|
||||
|
||||
prng_type*
|
||||
operator->() const
|
||||
{
|
||||
return p_;
|
||||
}
|
||||
};
|
||||
|
||||
#ifndef BOOST_BEAST_NO_THREAD_LOCAL
|
||||
static
|
||||
prng_ref
|
||||
prng()
|
||||
{
|
||||
static std::atomic<std::uint64_t> stream{0};
|
||||
thread_local prng_type p{seed(), stream++};
|
||||
return prng_ref(p);
|
||||
}
|
||||
|
||||
#else
|
||||
static
|
||||
prng_ref
|
||||
prng()
|
||||
{
|
||||
return prng_ref(pool::impl().acquire());
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static
|
||||
std::uint32_t const*
|
||||
seed(std::seed_seq* ss = nullptr)
|
||||
{
|
||||
static seed_data d(ss);
|
||||
return d.v;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
create_mask()
|
||||
{
|
||||
auto p = prng();
|
||||
if(secure_prng_)
|
||||
for(;;)
|
||||
if(auto key = p->secure())
|
||||
return key;
|
||||
for(;;)
|
||||
if(auto key = p->fast())
|
||||
return key;
|
||||
}
|
||||
|
||||
private:
|
||||
struct seed_data
|
||||
{
|
||||
std::uint32_t v[8];
|
||||
|
||||
explicit
|
||||
seed_data(std::seed_seq* pss)
|
||||
{
|
||||
if(! pss)
|
||||
{
|
||||
std::random_device g;
|
||||
std::seed_seq ss{
|
||||
g(), g(), g(), g(), g(), g(), g(), g()};
|
||||
ss.generate(v, v+8);
|
||||
}
|
||||
else
|
||||
{
|
||||
pss->generate(v, v+8);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef BOOST_BEAST_NO_THREAD_LOCAL
|
||||
class pool
|
||||
{
|
||||
prng_type* head_ = nullptr;
|
||||
std::atomic<std::uint64_t> n_{0};
|
||||
std::mutex m_;
|
||||
|
||||
public:
|
||||
~pool()
|
||||
{
|
||||
for(auto p = head_; p;)
|
||||
{
|
||||
auto next = p->next;
|
||||
p->~prng_type();
|
||||
boost::alignment::aligned_free(p);
|
||||
p = next;
|
||||
}
|
||||
}
|
||||
|
||||
prng_type&
|
||||
acquire()
|
||||
{
|
||||
for(;;)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_);
|
||||
if(! head_)
|
||||
break;
|
||||
auto p = head_;
|
||||
head_ = head_->next;
|
||||
return *p;
|
||||
}
|
||||
auto p = boost::alignment::aligned_alloc(
|
||||
16, sizeof(prng_type));
|
||||
if(! p)
|
||||
BOOST_THROW_EXCEPTION(std::bad_alloc{});
|
||||
return *(::new(p) prng_type(seed(), n_++));
|
||||
}
|
||||
|
||||
void
|
||||
release(prng_type& p)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_);
|
||||
p.next = head_;
|
||||
head_ = &p;
|
||||
}
|
||||
|
||||
static
|
||||
pool&
|
||||
impl()
|
||||
{
|
||||
static pool instance;
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
template<bool deflateSupported>
|
||||
struct stream_base : stream_prng
|
||||
{
|
||||
|
@@ -3511,7 +3511,7 @@ inline
|
||||
void
|
||||
seed_prng(std::seed_seq& ss)
|
||||
{
|
||||
detail::stream_prng::seed(&ss);
|
||||
detail::prng_seed(&ss);
|
||||
}
|
||||
|
||||
} // websocket
|
||||
|
@@ -17,6 +17,7 @@ add_executable (tests-beast-websocket
|
||||
${TEST_MAIN}
|
||||
Jamfile
|
||||
test.hpp
|
||||
_detail_prng.cpp
|
||||
accept.cpp
|
||||
close.cpp
|
||||
error.cpp
|
||||
|
@@ -8,6 +8,7 @@
|
||||
#
|
||||
|
||||
local SOURCES =
|
||||
_detail_prng.cpp
|
||||
accept.cpp
|
||||
close.cpp
|
||||
error.cpp
|
||||
|
69
test/beast/websocket/_detail_prng.cpp
Normal file
69
test/beast/websocket/_detail_prng.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright (w) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
// Official repository: https://github.com/boostorg/beast
|
||||
//
|
||||
|
||||
// Test that header file is self-contained.
|
||||
#include <boost/beast/websocket/detail/prng.hpp>
|
||||
|
||||
#include <boost/beast/_experimental/unit_test/suite.hpp>
|
||||
|
||||
namespace boost {
|
||||
namespace beast {
|
||||
namespace websocket {
|
||||
namespace detail {
|
||||
|
||||
class prng_test
|
||||
: public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
#if 0
|
||||
char const* name() const noexcept //override
|
||||
{
|
||||
return "boost.beast.websocket.detail.prng";
|
||||
}
|
||||
#endif
|
||||
|
||||
template <class F>
|
||||
void
|
||||
testPrng(F const& f)
|
||||
{
|
||||
{
|
||||
auto v = f()();
|
||||
BEAST_EXPECT(
|
||||
v >= (prng::ref::min)() &&
|
||||
v <= (prng::ref::max)());
|
||||
}
|
||||
{
|
||||
auto v = f()();
|
||||
BEAST_EXPECT(
|
||||
v >= (prng::ref::min)() &&
|
||||
v <= (prng::ref::max)());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testPrng([]{ return make_prng(true); });
|
||||
testPrng([]{ return make_prng(false); });
|
||||
testPrng([]{ return make_prng_no_tls(true); });
|
||||
testPrng([]{ return make_prng_no_tls(false); });
|
||||
#if ! BOOST_BEAST_NO_THREAD_LOCAL
|
||||
testPrng([]{ return make_prng_tls(true); });
|
||||
testPrng([]{ return make_prng_tls(false); });
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
//static prng_test t;
|
||||
BEAST_DEFINE_TESTSUITE(beast,websocket,prng);
|
||||
|
||||
} // detail
|
||||
} // websocket
|
||||
} // beast
|
||||
} // boost
|
Reference in New Issue
Block a user