...
This commit is contained in:
parent
daf4e1eab9
commit
9a2baa702d
@ -45,6 +45,12 @@ if (ASIO_LIBRARY)
|
||||
|
||||
add_compile_options(ASIO::ASIO)
|
||||
add_compile_definitions(PUBLIC USE_ASIO_LIBRARY)
|
||||
|
||||
option(OPENSSL_LIBRARY "Use openssl library for related ASIO-based implementation" ON)
|
||||
if (OPENSSL_LIBRARY)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
add_compile_definitions(PUBLIC USE_OPENSSL_WITH_ASIO)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
/*
|
||||
|
||||
ABSTRACT DEVICE COMPONENTS LIBRARY
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#include "../common/adc_traits.h"
|
||||
|
||||
namespace adc
|
||||
@ -13,20 +20,21 @@ namespace adc
|
||||
* endpoint: proto_mark://host_name:port_num/path
|
||||
* where "part" is optional for all protocol kinds;
|
||||
*
|
||||
* for "local" kind protocol the endpoint string must consists of
|
||||
* for the "local" kind protocol the endpoint string must consists of
|
||||
* only "proto_mark" and "host_name" fields
|
||||
* (e.g.: local://APP_UNIX_SOCKET)
|
||||
*
|
||||
* NOTE: "proto_mark" field is parsed as case-insensitive string!
|
||||
*
|
||||
*/
|
||||
|
||||
class AdcEndpoint
|
||||
{
|
||||
protected:
|
||||
public:
|
||||
static constexpr std::string_view protoHostDelim = "://";
|
||||
static constexpr std::string_view hostPortDelim = ":";
|
||||
static constexpr std::string_view portPathDelim = "/";
|
||||
|
||||
public:
|
||||
enum proto_id_t : uint8_t {
|
||||
PROTO_ID_LOCAL,
|
||||
PROTO_ID_TCP,
|
||||
@ -47,6 +55,50 @@ public:
|
||||
static constexpr std::array validProtoMarks = {protoMarkLocal, protoMarkTCP, protoMarkTLS,
|
||||
protoMarkUDP, protoMarkWS, protoMarkWSS};
|
||||
|
||||
|
||||
// factory methods
|
||||
|
||||
template <traits::adc_input_char_range R>
|
||||
AdcEndpoint createLocal(R&& path)
|
||||
{
|
||||
return AdcEndpoint(PROTO_ID_LOCAL, std::string_view(""), -1, std::forward<R>(path));
|
||||
}
|
||||
|
||||
template <traits::adc_input_char_range HT>
|
||||
AdcEndpoint createTCP(HT&& host, int port)
|
||||
{
|
||||
return AdcEndpoint(PROTO_ID_TCP, std::forward<HT>(host), port);
|
||||
}
|
||||
|
||||
#ifdef USE_OPENSSL_WITH_ASIO
|
||||
template <traits::adc_input_char_range HT>
|
||||
AdcEndpoint createTLS(HT&& host, int port)
|
||||
{
|
||||
return AdcEndpoint(PROTO_ID_TLS, std::forward<HT>(host), port);
|
||||
}
|
||||
#endif
|
||||
|
||||
template <traits::adc_input_char_range HT>
|
||||
AdcEndpoint createUDP(HT&& host, int port)
|
||||
{
|
||||
return AdcEndpoint(PROTO_ID_UDP, std::forward<HT>(host), port);
|
||||
}
|
||||
|
||||
|
||||
template <traits::adc_input_char_range HT, traits::adc_input_char_range PTT = std::string_view>
|
||||
AdcEndpoint createWS(HT&& host, int port, PTT&& path = PTT())
|
||||
{
|
||||
return AdcEndpoint(PROTO_ID_WS, std::forward<HT>(host), port, std::forward<PTT>(path));
|
||||
}
|
||||
|
||||
|
||||
template <traits::adc_input_char_range HT, traits::adc_input_char_range PTT = std::string_view>
|
||||
AdcEndpoint createWSS(HT&& host, int port, PTT&& path = PTT())
|
||||
{
|
||||
return AdcEndpoint(PROTO_ID_WSS, std::forward<HT>(host), port, std::forward<PTT>(path));
|
||||
}
|
||||
|
||||
|
||||
/* Constructors and destructor */
|
||||
|
||||
AdcEndpoint() = default;
|
||||
@ -255,8 +307,10 @@ public:
|
||||
|
||||
auto sz = std::distance(r.begin(), found.begin());
|
||||
|
||||
std::string proto;
|
||||
std::ranges::copy(r | std::views::take(sz), std::back_inserter(proto));
|
||||
std::string proto; // case-insensitive!
|
||||
std::ranges::copy(
|
||||
r | std::views::take(sz) | std::views::transform([](auto ch) -> char { return std::tolower(ch); }),
|
||||
std::back_inserter(proto));
|
||||
|
||||
std::underlying_type_t<proto_id_t> i = 0;
|
||||
proto_id_t id = PROTO_ID_UNKNOWN;
|
||||
|
||||
139
net/adc_netmsg.h
139
net/adc_netmsg.h
@ -43,91 +43,17 @@ void convertToBytes(ByteStorageT& res, const T& v, const Ts&... vs)
|
||||
} // namespace utils
|
||||
|
||||
|
||||
namespace traits
|
||||
{
|
||||
|
||||
template <typename T, typename IT>
|
||||
concept adc_netmessage_c = requires(const T t) { // const methods
|
||||
requires std::same_as<std::iter_value_t<IT>, char>;
|
||||
{ t.empty() } -> std::convertible_to<bool>;
|
||||
{ t.byteSize() } -> std::convertible_to<size_t>;
|
||||
{ t.bytes() } -> adc_output_char_range;
|
||||
{ t.byteView() } -> adc_range_of_view_char_range;
|
||||
{ t.setFromBytes(std::input_iterator<IT>) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
} // namespace traits
|
||||
|
||||
|
||||
/*
|
||||
Trivial message interface:
|
||||
byte storage: just a char-range
|
||||
*/
|
||||
*/
|
||||
template <traits::adc_output_char_range ByteStorageT = std::string, traits::adc_char_view ByteViewT = std::string_view>
|
||||
class AdcNetMessageTrivialInterface
|
||||
class AdcNetMessageInterface
|
||||
{
|
||||
public:
|
||||
virtual ~AdcNetMessageTrivialInterface() = default;
|
||||
typedef ByteStorageT byte_storage_t;
|
||||
typedef ByteViewT byte_view_t;
|
||||
|
||||
bool empty() const { return std::ranges::distance(_bytes.begin(), _bytes.end()) == 0; }
|
||||
|
||||
size_t byteSize() const
|
||||
{
|
||||
//
|
||||
return std::ranges::distance(_bytes.begin(), _bytes.end());
|
||||
}
|
||||
|
||||
// get a copy of message bytes
|
||||
template <traits::adc_output_char_range R>
|
||||
R bytes() const
|
||||
{
|
||||
R r;
|
||||
std::ranges::copy(_bytes, std::back_inserter(r));
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
virtual ByteStorageT bytes() const
|
||||
{
|
||||
//
|
||||
return bytes<ByteStorageT>();
|
||||
}
|
||||
|
||||
// get a view of message bytes
|
||||
template <traits::adc_range_of_view_char_range R>
|
||||
R bytesView() const
|
||||
{
|
||||
R r;
|
||||
|
||||
r.emplace_back(_bytes.begin(), _bytes.end());
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
std::vector<ByteViewT> bytesView() const
|
||||
{
|
||||
//
|
||||
return bytesView<std::vector<ByteViewT>>();
|
||||
}
|
||||
|
||||
protected:
|
||||
ByteStorageT _bytes;
|
||||
|
||||
AdcNetMessageTrivialInterface() = default;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
interface for more complex messages:
|
||||
byte storage: std::vector of char-range (sequence of buffers)
|
||||
*/
|
||||
template <traits::adc_output_char_range ByteStorageT = std::string, traits::adc_char_view ByteViewT = std::string_view>
|
||||
class AdcNetMessageSeqInterface
|
||||
{
|
||||
public:
|
||||
virtual ~AdcNetMessageSeqInterface() = default;
|
||||
virtual ~AdcNetMessageInterface() = default;
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
@ -201,11 +127,11 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<ByteStorageT> _bytes;
|
||||
std::vector<ByteStorageT> _bytes; // sequence of byte buffers
|
||||
size_t _reservedNum;
|
||||
|
||||
|
||||
AdcNetMessageSeqInterface(size_t reserved = 0) : _reservedNum(reserved), _bytes()
|
||||
AdcNetMessageInterface(size_t reserved = 0) : _reservedNum(reserved), _bytes()
|
||||
{
|
||||
// reserve the "_reservedNum" first elements
|
||||
_bytes.resize(_reservedNum);
|
||||
@ -220,16 +146,24 @@ protected:
|
||||
|
||||
// Generic message class
|
||||
template <traits::adc_output_char_range ByteStorageT = std::string, traits::adc_char_view ByteViewT = std::string_view>
|
||||
class AdcGenericNetMessage : public AdcNetMessageTrivialInterface<ByteStorageT, ByteViewT>
|
||||
class AdcGenericNetMessage : public AdcNetMessageInterface<ByteStorageT, ByteViewT>
|
||||
{
|
||||
using base_t = AdcNetMessageTrivialInterface<ByteStorageT, ByteViewT>;
|
||||
using base_t = AdcNetMessageInterface<ByteStorageT, ByteViewT>;
|
||||
|
||||
public:
|
||||
using typename base_t::byte_storage_t;
|
||||
using typename base_t::byte_view_t;
|
||||
|
||||
using base_t::base_t;
|
||||
using base_t::bytes;
|
||||
using base_t::bytesView;
|
||||
using base_t::empty;
|
||||
|
||||
AdcGenericNetMessage() : base_t()
|
||||
{
|
||||
// just the single buffer
|
||||
this->_bytes.resize(1);
|
||||
}
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
AdcGenericNetMessage(const T& v, const Ts&... vs) : AdcGenericNetMessage()
|
||||
@ -241,13 +175,13 @@ public:
|
||||
template <typename T, typename... Ts>
|
||||
void appendBytes(const T& v, const Ts&... vs)
|
||||
{
|
||||
utils::convertToBytes(this->_bytes, v, vs...);
|
||||
utils::convertToBytes(this->_bytes[0], v, vs...);
|
||||
}
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
void setBytes(const T& v, const Ts&... vs)
|
||||
{
|
||||
this->_bytes = ByteStorageT();
|
||||
this->_bytes[0] = ByteStorageT();
|
||||
|
||||
appendBytes(v, vs...);
|
||||
}
|
||||
@ -257,7 +191,7 @@ public:
|
||||
void appendFromBytes(IT begin, IT end)
|
||||
requires std::same_as<std::iter_value_t<IT>, char>
|
||||
{
|
||||
std::copy(begin, end, std::back_inserter(this->_bytes));
|
||||
std::copy(begin, end, std::back_inserter(this->_bytes[0]));
|
||||
}
|
||||
|
||||
|
||||
@ -265,7 +199,7 @@ public:
|
||||
void setFromBytes(IT begin, IT end)
|
||||
requires std::same_as<std::iter_value_t<IT>, char>
|
||||
{
|
||||
this->_bytes = ByteStorageT();
|
||||
this->_bytes[0] = ByteStorageT();
|
||||
|
||||
appendFromBytes(begin, end);
|
||||
}
|
||||
@ -283,10 +217,10 @@ static constexpr char ADC_DEFAULT_KEY_TOKEN_DELIMITER[] = " ";
|
||||
template <const char* TOKEN_DELIM = constants::ADC_DEFAULT_TOKEN_DELIMITER,
|
||||
traits::adc_output_char_range ByteStorageT = std::string,
|
||||
traits::adc_char_view ByteViewT = std::string_view>
|
||||
class AdcTokenNetMessage : public AdcNetMessageSeqInterface<ByteStorageT, ByteViewT>
|
||||
class AdcTokenNetMessage : public AdcNetMessageInterface<ByteStorageT, ByteViewT>
|
||||
{
|
||||
// AdcNetMessageSeqInterface<ByteStorageT, ByteViewT>::_bytes - tokens
|
||||
using base_t = AdcNetMessageSeqInterface<ByteStorageT, ByteViewT>;
|
||||
using base_t = AdcNetMessageInterface<ByteStorageT, ByteViewT>;
|
||||
|
||||
public:
|
||||
static constexpr std::string_view tokenDelimiter{TOKEN_DELIM};
|
||||
@ -457,7 +391,7 @@ public:
|
||||
using base_t::tokens;
|
||||
|
||||
|
||||
AdcKeyTokenNetMessage()
|
||||
AdcKeyTokenNetMessage() : base_t()
|
||||
{
|
||||
this->_reservedNum = 1; // reserve the first element for keyword
|
||||
this->_bytes.resize(this->_reservedNum);
|
||||
@ -599,4 +533,29 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
namespace traits
|
||||
{
|
||||
|
||||
template <typename T>
|
||||
concept adc_netmessage_c = requires {
|
||||
typename T::byte_storage_t;
|
||||
typename T::byte_view_t;
|
||||
std::derived_from<T, AdcNetMessageInterface<typename T::byte_storage_t, typename T::byte_view_t>>;
|
||||
};
|
||||
// template <typename T, typename IT>
|
||||
// concept adc_netmessage_c = requires(const T t) { // const methods
|
||||
// requires std::same_as<std::iter_value_t<IT>, char>;
|
||||
// { t.empty() } -> std::convertible_to<bool>;
|
||||
// { t.byteSize() } -> std::convertible_to<size_t>;
|
||||
// { t.bytes() } -> adc_output_char_range;
|
||||
// { t.byteView() } -> adc_range_of_view_char_range;
|
||||
// { t.setFromBytes(std::input_iterator<IT>) } -> std::same_as<void>;
|
||||
// };
|
||||
|
||||
} // namespace traits
|
||||
|
||||
|
||||
|
||||
} // namespace adc
|
||||
|
||||
@ -11,12 +11,13 @@ ABSTRACT DEVICE COMPONENTS LIBRARY
|
||||
#include <utility>
|
||||
|
||||
|
||||
#include "adc_netmsg.h"
|
||||
|
||||
namespace adc
|
||||
{
|
||||
|
||||
|
||||
template <typename NetMessageT, typename ImplT>
|
||||
template <typename ImplT>
|
||||
class AdcNetService
|
||||
{
|
||||
protected:
|
||||
@ -56,13 +57,13 @@ public:
|
||||
}
|
||||
|
||||
|
||||
template <typename... ArgTs>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename... ArgTs>
|
||||
auto asyncSend(const NetMessageT& msg, const timeout_drtn_t& timeout = defaultSendTimeout, ArgTs&&... args)
|
||||
{
|
||||
return _impl.asyncSend(msg, timeout, std::forward<ArgTs>(args)...);
|
||||
}
|
||||
|
||||
template <typename... ArgTs>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename... ArgTs>
|
||||
auto asyncReceive(const timeout_drtn_t& timeout = defaultRecvTimeout, ArgTs&&... args)
|
||||
{
|
||||
return _impl.asyncReceive(timeout, std::forward<ArgTs>(args)...);
|
||||
@ -78,14 +79,14 @@ public:
|
||||
return _impl.connect(endpoint, timeout, std::forward<ArgTs>(args)...);
|
||||
}
|
||||
|
||||
template <typename... ArgTs>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename... ArgTs>
|
||||
auto send(const NetMessageT& msg, const timeout_drtn_t& timeout = defaultSendTimeout, ArgTs&&... args)
|
||||
{
|
||||
return _impl.send(msg, timeout, std::forward<ArgTs>(args)...);
|
||||
}
|
||||
|
||||
|
||||
template <typename... ArgTs>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename... ArgTs>
|
||||
NetMessageT receive(const timeout_drtn_t& timeout = defaultRecvTimeout, ArgTs&&... args)
|
||||
{
|
||||
return _impl.receive(timeout, std::forward<ArgTs>(args)...);
|
||||
@ -102,11 +103,11 @@ public:
|
||||
namespace traits
|
||||
{
|
||||
|
||||
// template <typename T>
|
||||
// concept adc_netservice_c = requires {
|
||||
// typename T::impl_t;
|
||||
// std::derived_from<AdcNetService<typename T::impl_t>>;
|
||||
// };
|
||||
template <typename T>
|
||||
concept adc_netservice_c = requires {
|
||||
typename T::impl_t;
|
||||
std::derived_from<T, AdcNetService<typename T::impl_t>>;
|
||||
};
|
||||
|
||||
} // namespace traits
|
||||
|
||||
|
||||
@ -10,15 +10,19 @@
|
||||
|
||||
|
||||
#include <future>
|
||||
#include "adc_netservice.h"
|
||||
#ifdef USE_ASIO_LIBRARY
|
||||
|
||||
#include <asio/awaitable.hpp>
|
||||
// #include <asio/awaitable.hpp>
|
||||
#include <asio/basic_datagram_socket.hpp>
|
||||
#include <asio/basic_seq_packet_socket.hpp>
|
||||
#include <asio/basic_stream_socket.hpp>
|
||||
#include <asio/compose.hpp>
|
||||
#include <asio/experimental/awaitable_operators.hpp>
|
||||
// #include <asio/experimental/awaitable_operators.hpp>
|
||||
#include <asio/ip/tcp.hpp>
|
||||
#include <asio/read_until.hpp>
|
||||
#include <asio/ssl.hpp>
|
||||
#include <asio/ssl/stream.hpp>
|
||||
#include <asio/steady_timer.hpp>
|
||||
#include <asio/streambuf.hpp>
|
||||
#include <asio/use_future.hpp>
|
||||
@ -26,11 +30,14 @@
|
||||
|
||||
#include <concepts>
|
||||
|
||||
|
||||
#include "adc_netmsg.h"
|
||||
|
||||
namespace adc::impl
|
||||
{
|
||||
|
||||
|
||||
template <typename NetMessageT, typename InetProtoT>
|
||||
template <typename InetProtoT>
|
||||
class AdcNetServiceASIO : public InetProtoT
|
||||
{
|
||||
public:
|
||||
@ -63,7 +70,12 @@ public:
|
||||
switch (state) {
|
||||
case starting:
|
||||
state = cancel_timer;
|
||||
return _socket.async_connect(endpoint, std::move(self));
|
||||
if constexpr (std::derived_from<socket_t,
|
||||
asio::ssl::stream<typename socket_t::lowest_layer_type>>) {
|
||||
return _socket.lowest_layer().async_connect(endpoint, std::move(self));
|
||||
} else {
|
||||
return _socket.async_connect(endpoint, std::move(self));
|
||||
}
|
||||
break;
|
||||
case cancel_timer:
|
||||
timer->cancel();
|
||||
@ -78,7 +90,9 @@ public:
|
||||
token, _socket);
|
||||
}
|
||||
|
||||
template <typename TimeoutT, asio::completion_token_for<void(std::error_code)> CompletionTokenT>
|
||||
template <traits::adc_netmessage_c NetMessageT,
|
||||
typename TimeoutT,
|
||||
asio::completion_token_for<void(std::error_code)> CompletionTokenT>
|
||||
auto asynSend(const NetMessageT& msg, const TimeoutT& timeout, CompletionTokenT&& token)
|
||||
{
|
||||
enum { starting, cancel_timer };
|
||||
@ -93,10 +107,11 @@ public:
|
||||
// wrapper
|
||||
return asio::async_compose<CompletionTokenT, void(std::error_code)>(
|
||||
[buff = std::move(buff), timer = std::move(timer), state = starting, this](
|
||||
auto& self, const std::error_code& ec = {}, size_t sz = 0) mutable {
|
||||
auto& self, const std::error_code& ec = {}, size_t = 0) mutable {
|
||||
if (!ec) {
|
||||
switch (state) {
|
||||
case starting:
|
||||
state = cancel_timer;
|
||||
if constexpr (std::derived_from<
|
||||
socket_t, asio::basic_stream_socket<typename socket_t::protocol_type>>) {
|
||||
return asio::async_write(_socket, buff, std::move(self));
|
||||
@ -124,7 +139,7 @@ public:
|
||||
}
|
||||
|
||||
|
||||
template <typename TimeoutT, typename CompletionTokenT>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename TimeoutT, typename CompletionTokenT>
|
||||
auto asyncReceive(const TimeoutT& timeout, CompletionTokenT&& token)
|
||||
{
|
||||
enum { starting, cancel_timer };
|
||||
@ -135,15 +150,16 @@ public:
|
||||
|
||||
return asio::async_compose<CompletionTokenT, void(const std::error_code&, const NetMessageT&)>(
|
||||
[timer = std::move(timer), out_flags = std::move(out_flags), state = starting, this](
|
||||
auto& self, const std::error_code& ec = {}, size_t sz = 0) mutable {
|
||||
auto& self, const std::error_code& ec = {}, size_t = 0) mutable {
|
||||
if (!ec) {
|
||||
switch (state) {
|
||||
case starting:
|
||||
state = cancel_timer;
|
||||
if constexpr (std::derived_from<
|
||||
socket_t, asio::basic_stream_socket<typename socket_t::protocol_type>>) {
|
||||
return asio::async_read_until(
|
||||
_socket, _streamBuffer,
|
||||
[this](auto begin, auto end) { this->matchCondition(begin, end); },
|
||||
[this](auto begin, auto end) { return this->matchCondition(begin, end); },
|
||||
std::move(self));
|
||||
} else if constexpr (std::derived_from<socket_t, asio::basic_datagram_socket<
|
||||
typename socket_t::protocol_type>>) {
|
||||
@ -200,20 +216,43 @@ public:
|
||||
ftr.get();
|
||||
}
|
||||
|
||||
template <typename TimeoutT>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename TimeoutT>
|
||||
auto send(const NetMessageT& msg, const TimeoutT& timeout)
|
||||
{
|
||||
std::future<void> ftr = asyncSend(msg, timeout, asio::use_future);
|
||||
ftr.get();
|
||||
}
|
||||
|
||||
template <typename TimeoutT>
|
||||
template <traits::adc_netmessage_c NetMessageT, typename TimeoutT>
|
||||
auto receive(const TimeoutT& timeout)
|
||||
{
|
||||
std::future<NetMessageT> ftr = asyncReceive(timeout, asio::use_future);
|
||||
return ftr.get();
|
||||
}
|
||||
|
||||
|
||||
std::error_code close(asio::socket_base::shutdown_type stype = asio::socket_base::shutdown_both)
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
if constexpr (std::derived_from<socket_t, asio::ssl::stream<typename socket_t::lowest_layer_type>>) {
|
||||
_socket.shutdown(ec); // shutdown OpenSSL stream
|
||||
if (!ec) {
|
||||
_socket.lowest_layer().shutdown(stype, ec);
|
||||
if (!ec) {
|
||||
_socket.lowest_layer().close(ec);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_socket.shutdown(stype, ec);
|
||||
if (!ec) {
|
||||
_socket.close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
protected:
|
||||
socket_t& _socket;
|
||||
|
||||
@ -242,4 +281,11 @@ protected:
|
||||
} // namespace adc::impl
|
||||
|
||||
|
||||
namespace adc
|
||||
{
|
||||
|
||||
typedef AdcNetService<impl::AdcNetServiceASIO<asio::ip::tcp>> AdcNetServiceAsioTcp;
|
||||
|
||||
} // namespace adc
|
||||
|
||||
#endif
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user