Bonjour - resolve hostname to IP using mdns.
Octoprint - on Windows use Bonjour resolving to get IP address before the http connection. Only use Windows address resolving if Bonjour couldn't resolve.
This commit is contained in:
parent
fbdf777898
commit
d2c6f597d2
@ -13,6 +13,8 @@
|
|||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <boost/date_time/posix_time/posix_time_duration.hpp>
|
#include <boost/date_time/posix_time/posix_time_duration.hpp>
|
||||||
#include <boost/format.hpp>
|
#include <boost/format.hpp>
|
||||||
|
#include <boost/thread.hpp>
|
||||||
|
#include <boost/enable_shared_from_this.hpp>
|
||||||
|
|
||||||
using boost::optional;
|
using boost::optional;
|
||||||
using boost::system::error_code;
|
using boost::system::error_code;
|
||||||
@ -238,6 +240,7 @@ struct DnsRR_A
|
|||||||
enum { TAG = 0x1 };
|
enum { TAG = 0x1 };
|
||||||
|
|
||||||
asio::ip::address_v4 ip;
|
asio::ip::address_v4 ip;
|
||||||
|
std::string name;
|
||||||
|
|
||||||
static void decode(optional<DnsRR_A> &result, const DnsResource &rr)
|
static void decode(optional<DnsRR_A> &result, const DnsResource &rr)
|
||||||
{
|
{
|
||||||
@ -255,6 +258,7 @@ struct DnsRR_AAAA
|
|||||||
enum { TAG = 0x1c };
|
enum { TAG = 0x1c };
|
||||||
|
|
||||||
asio::ip::address_v6 ip;
|
asio::ip::address_v6 ip;
|
||||||
|
std::string name;
|
||||||
|
|
||||||
static void decode(optional<DnsRR_AAAA> &result, const DnsResource &rr)
|
static void decode(optional<DnsRR_AAAA> &result, const DnsResource &rr)
|
||||||
{
|
{
|
||||||
@ -438,8 +442,14 @@ private:
|
|||||||
void parse_rr(const std::vector<char> &buffer, DnsResource &&rr, size_t dataoffset, const Bonjour::TxtKeys &txt_keys)
|
void parse_rr(const std::vector<char> &buffer, DnsResource &&rr, size_t dataoffset, const Bonjour::TxtKeys &txt_keys)
|
||||||
{
|
{
|
||||||
switch (rr.type) {
|
switch (rr.type) {
|
||||||
case DnsRR_A::TAG: DnsRR_A::decode(this->rr_a, rr); break;
|
case DnsRR_A::TAG:
|
||||||
case DnsRR_AAAA::TAG: DnsRR_AAAA::decode(this->rr_aaaa, rr); break;
|
DnsRR_A::decode(this->rr_a, rr);
|
||||||
|
this->rr_a->name = rr.name;
|
||||||
|
break;
|
||||||
|
case DnsRR_AAAA::TAG:
|
||||||
|
DnsRR_AAAA::decode(this->rr_aaaa, rr);
|
||||||
|
this->rr_aaaa->name = rr.name;
|
||||||
|
break;
|
||||||
case DnsRR_SRV::TAG: {
|
case DnsRR_SRV::TAG: {
|
||||||
auto srv = DnsRR_SRV::decode(buffer, rr, dataoffset);
|
auto srv = DnsRR_SRV::decode(buffer, rr, dataoffset);
|
||||||
if (srv) { this->sdmap.insert_srv(std::move(rr.name), std::move(*srv)); }
|
if (srv) { this->sdmap.insert_srv(std::move(rr.name), std::move(*srv)); }
|
||||||
@ -484,20 +494,23 @@ std::ostream& operator<<(std::ostream &os, const DnsMessage &msg)
|
|||||||
struct BonjourRequest
|
struct BonjourRequest
|
||||||
{
|
{
|
||||||
static const asio::ip::address_v4 MCAST_IP4;
|
static const asio::ip::address_v4 MCAST_IP4;
|
||||||
|
static const asio::ip::address_v6 MCAST_IP6;
|
||||||
static const uint16_t MCAST_PORT;
|
static const uint16_t MCAST_PORT;
|
||||||
|
|
||||||
std::vector<char> data;
|
std::vector<char> data;
|
||||||
|
|
||||||
static optional<BonjourRequest> make(const std::string &service, const std::string &protocol);
|
static optional<BonjourRequest> make_PTR(const std::string &service, const std::string &protocol);
|
||||||
|
static optional<BonjourRequest> make_A(const std::string& hostname);
|
||||||
|
static optional<BonjourRequest> make_AAAA(const std::string& hostname);
|
||||||
private:
|
private:
|
||||||
BonjourRequest(std::vector<char> &&data) : data(std::move(data)) {}
|
BonjourRequest(std::vector<char> &&data) : data(std::move(data)) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asio::ip::address_v4 BonjourRequest::MCAST_IP4{0xe00000fb};
|
const asio::ip::address_v4 BonjourRequest::MCAST_IP4{ 0xe00000fb };
|
||||||
|
const asio::ip::address_v6 BonjourRequest::MCAST_IP6 = asio::ip::make_address_v6("ff02::fb");
|
||||||
const uint16_t BonjourRequest::MCAST_PORT = 5353;
|
const uint16_t BonjourRequest::MCAST_PORT = 5353;
|
||||||
|
|
||||||
optional<BonjourRequest> BonjourRequest::make(const std::string &service, const std::string &protocol)
|
optional<BonjourRequest> BonjourRequest::make_PTR(const std::string &service, const std::string &protocol)
|
||||||
{
|
{
|
||||||
if (service.size() > 15 || protocol.size() > 15) {
|
if (service.size() > 15 || protocol.size() > 15) {
|
||||||
return boost::none;
|
return boost::none;
|
||||||
@ -535,6 +548,75 @@ optional<BonjourRequest> BonjourRequest::make(const std::string &service, const
|
|||||||
return BonjourRequest(std::move(data));
|
return BonjourRequest(std::move(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optional<BonjourRequest> BonjourRequest::make_A(const std::string& hostname)
|
||||||
|
{
|
||||||
|
// todo: why is this and what is real max
|
||||||
|
if (hostname.size() > 30) {
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> data;
|
||||||
|
data.reserve(hostname.size() + 18);
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
static const unsigned char rq_meta[] = {
|
||||||
|
0x00, 0x00, // Query ID (zero for mDNS)
|
||||||
|
0x00, 0x00, // Flags
|
||||||
|
0x00, 0x01, // One query
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Zero Answer, Authority, and Additional RRs
|
||||||
|
};
|
||||||
|
std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data));
|
||||||
|
|
||||||
|
// Add hostname without .local
|
||||||
|
data.push_back(hostname.size());
|
||||||
|
data.insert(data.end(), hostname.begin(), hostname.end());
|
||||||
|
|
||||||
|
// Add the rest of A record
|
||||||
|
static const unsigned char ptr_tail[] = {
|
||||||
|
0x05, // length of "local"
|
||||||
|
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00,// "local" string and terminator
|
||||||
|
0x00, 0x01, // Type A
|
||||||
|
0x00, 0xff, // Class - 01 is internet 0xff is any
|
||||||
|
};
|
||||||
|
std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data));
|
||||||
|
|
||||||
|
return BonjourRequest(std::move(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<BonjourRequest> BonjourRequest::make_AAAA(const std::string& hostname)
|
||||||
|
{
|
||||||
|
// todo: why is this and what is real max
|
||||||
|
if (hostname.size() > 30) {
|
||||||
|
return boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> data;
|
||||||
|
data.reserve(hostname.size() + 18);
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
static const unsigned char rq_meta[] = {
|
||||||
|
0x00, 0x00, // Query ID (zero for mDNS)
|
||||||
|
0x00, 0x00, // Flags
|
||||||
|
0x00, 0x01, // One query
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Zero Answer, Authority, and Additional RRs
|
||||||
|
};
|
||||||
|
std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data));
|
||||||
|
|
||||||
|
// Add hostname without .local
|
||||||
|
data.push_back(hostname.size());
|
||||||
|
data.insert(data.end(), hostname.begin(), hostname.end());
|
||||||
|
|
||||||
|
// Add the rest of A record
|
||||||
|
static const unsigned char ptr_tail[] = {
|
||||||
|
0x05, // length of "local"
|
||||||
|
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, // "local" string and terminator
|
||||||
|
0x00, 0x1c, // Type AAAA
|
||||||
|
0x00, 0xff, // Class - 01 is internet 0xff is any
|
||||||
|
};
|
||||||
|
std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data));
|
||||||
|
|
||||||
|
return BonjourRequest(std::move(data));
|
||||||
|
}
|
||||||
|
|
||||||
// API - private part
|
// API - private part
|
||||||
|
|
||||||
@ -546,17 +628,22 @@ struct Bonjour::priv
|
|||||||
TxtKeys txt_keys;
|
TxtKeys txt_keys;
|
||||||
unsigned timeout;
|
unsigned timeout;
|
||||||
unsigned retries;
|
unsigned retries;
|
||||||
|
std::string hostname;
|
||||||
|
|
||||||
|
std::vector<BonjourReply> replies;
|
||||||
|
|
||||||
std::vector<char> buffer;
|
std::vector<char> buffer;
|
||||||
std::thread io_thread;
|
std::thread io_thread;
|
||||||
Bonjour::ReplyFn replyfn;
|
Bonjour::ReplyFn replyfn;
|
||||||
Bonjour::CompleteFn completefn;
|
Bonjour::CompleteFn completefn;
|
||||||
|
Bonjour::ResolveFn resolvefn;
|
||||||
|
|
||||||
priv(std::string &&service);
|
priv(std::string &&service);
|
||||||
|
|
||||||
std::string strip_service_dn(const std::string &service_name) const;
|
std::string strip_service_dn(const std::string &service_name) const;
|
||||||
void udp_receive(udp::endpoint from, size_t bytes);
|
void udp_receive_lookup(udp::endpoint from, size_t bytes);
|
||||||
void lookup_perform();
|
void lookup_perform();
|
||||||
|
void resolve_perform();
|
||||||
};
|
};
|
||||||
|
|
||||||
Bonjour::priv::priv(std::string &&service)
|
Bonjour::priv::priv(std::string &&service)
|
||||||
@ -582,7 +669,7 @@ std::string Bonjour::priv::strip_service_dn(const std::string &service_name) con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes)
|
void Bonjour::priv::udp_receive_lookup(udp::endpoint from, size_t bytes)
|
||||||
{
|
{
|
||||||
if (bytes == 0 || !replyfn) {
|
if (bytes == 0 || !replyfn) {
|
||||||
return;
|
return;
|
||||||
@ -621,7 +708,7 @@ void Bonjour::priv::lookup_perform()
|
|||||||
{
|
{
|
||||||
service_dn = (boost::format("_%1%._%2%.local") % service % protocol).str();
|
service_dn = (boost::format("_%1%._%2%.local") % service % protocol).str();
|
||||||
|
|
||||||
const auto brq = BonjourRequest::make(service, protocol);
|
const auto brq = BonjourRequest::make_PTR(service, protocol);
|
||||||
if (!brq) {
|
if (!brq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -659,7 +746,7 @@ void Bonjour::priv::lookup_perform()
|
|||||||
|
|
||||||
udp::endpoint recv_from;
|
udp::endpoint recv_from;
|
||||||
const auto recv_handler = [&](const error_code &error, size_t bytes) {
|
const auto recv_handler = [&](const error_code &error, size_t bytes) {
|
||||||
if (!error) { self->udp_receive(recv_from, bytes); }
|
if (!error) { self->udp_receive_lookup(recv_from, bytes); }
|
||||||
};
|
};
|
||||||
socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler);
|
socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler);
|
||||||
|
|
||||||
@ -678,6 +765,282 @@ void Bonjour::priv::lookup_perform()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ResolveSocket;
|
||||||
|
// One session for socket::async_receive_from().
|
||||||
|
// Keeps buffer of data so it is not overwritten when another async_receive_from starts.
|
||||||
|
struct UdpSession : boost::enable_shared_from_this<UdpSession>{
|
||||||
|
|
||||||
|
UdpSession(ResolveSocket* sckt, Bonjour::ReplyFn rfn) : socket(sckt), replyfn(rfn)
|
||||||
|
{
|
||||||
|
buffer.resize(DnsMessage::MAX_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_receive(const error_code& error, size_t bytes);
|
||||||
|
|
||||||
|
udp::endpoint remote_endpoint;
|
||||||
|
std::vector<char> buffer;
|
||||||
|
ResolveSocket* socket;
|
||||||
|
Bonjour::ReplyFn replyfn;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Udp socket for Bonjour::resolve_perform().
|
||||||
|
// Starts receiving answers after first send() call until io_service is stopped.
|
||||||
|
class ResolveSocket
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
typedef boost::shared_ptr<UdpSession> SharedSession;
|
||||||
|
Bonjour::ReplyFn replyfn; // this doesnt call same fn as replyfn of Bonjour class
|
||||||
|
asio::ip::address multicast_address;
|
||||||
|
udp::socket socket;
|
||||||
|
udp::endpoint mcast_endpoint;
|
||||||
|
boost::shared_ptr< boost::asio::io_service > io_service;
|
||||||
|
std::string hostname;
|
||||||
|
boost::optional<BonjourRequest> request_A;
|
||||||
|
boost::optional<BonjourRequest> request_AAAA;
|
||||||
|
asio::ip::multicast::outbound_interface outbound_interface;
|
||||||
|
public:
|
||||||
|
ResolveSocket(const std::string& hostname, Bonjour::ReplyFn replyfn, const asio::ip::address& multicast_address, const asio::ip::address& interface_address, boost::shared_ptr< boost::asio::io_service > io_service);
|
||||||
|
|
||||||
|
void send();
|
||||||
|
void async_receive();
|
||||||
|
void cancel() { socket.cancel(); }
|
||||||
|
std::string get_hostname() { return hostname; }
|
||||||
|
private:
|
||||||
|
void receive_handler(SharedSession session, const error_code& error, size_t bytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
ResolveSocket::ResolveSocket(const std::string& hostname, Bonjour::ReplyFn replyfn, const asio::ip::address& multicast_address, const asio::ip::address& interface_address, boost::shared_ptr< boost::asio::io_service > io_service)
|
||||||
|
: hostname(hostname)
|
||||||
|
, replyfn(replyfn)
|
||||||
|
, multicast_address(multicast_address)
|
||||||
|
, socket(*io_service)
|
||||||
|
, io_service(io_service)
|
||||||
|
{
|
||||||
|
assert(!hostname.empty() && replyfn);
|
||||||
|
try {
|
||||||
|
// open socket
|
||||||
|
boost::asio::ip::udp::endpoint listen_endpoint(multicast_address.is_v4() ? udp::v4() : udp::v6(), BonjourRequest::MCAST_PORT);
|
||||||
|
socket.open(listen_endpoint.protocol());
|
||||||
|
// set socket to listen
|
||||||
|
socket.set_option(udp::socket::reuse_address(true));
|
||||||
|
socket.bind(listen_endpoint);
|
||||||
|
if (interface_address.is_v4()) {
|
||||||
|
// listen for multicast on given interface
|
||||||
|
socket.set_option(boost::asio::ip::multicast::join_group(multicast_address.to_v4(), interface_address.to_v4()));
|
||||||
|
// send to interface
|
||||||
|
socket.set_option(asio::ip::multicast::outbound_interface(interface_address.to_v4()));
|
||||||
|
} else {
|
||||||
|
// listen for multicast on given interface
|
||||||
|
socket.set_option(boost::asio::ip::multicast::join_group(multicast_address.to_v6(), interface_address.to_v6().scope_id()));
|
||||||
|
// send to interface
|
||||||
|
socket.set_option(asio::ip::multicast::outbound_interface(interface_address.to_v6().scope_id()));
|
||||||
|
}
|
||||||
|
mcast_endpoint = udp::endpoint(multicast_address, BonjourRequest::MCAST_PORT);
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "Socket created. Multicast: " << multicast_address << ". Interface: " << interface_address;
|
||||||
|
}
|
||||||
|
catch (std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveSocket::send()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!request_A) {
|
||||||
|
// BonjourRequest::make_A / AAAA is now implemented to add .local correctly after the hostname.
|
||||||
|
// If that is unsufficient, we need to change make_A / AAAA and pass full hostname.
|
||||||
|
std::string trimmed_hostname = hostname;
|
||||||
|
if (size_t dot_pos = trimmed_hostname.find_first_of('.'); dot_pos != std::string::npos)
|
||||||
|
trimmed_hostname = trimmed_hostname.substr(0, dot_pos);
|
||||||
|
request_A = BonjourRequest::make_A(trimmed_hostname);
|
||||||
|
}
|
||||||
|
if (!request_AAAA) {
|
||||||
|
std::string trimmed_hostname = hostname;
|
||||||
|
if (size_t dot_pos = trimmed_hostname.find_first_of('.'); dot_pos != std::string::npos)
|
||||||
|
trimmed_hostname = trimmed_hostname.substr(0, dot_pos);
|
||||||
|
request_AAAA = BonjourRequest::make_AAAA(trimmed_hostname);
|
||||||
|
}
|
||||||
|
// multicast both queries
|
||||||
|
socket.send_to(asio::buffer(request_A->data), mcast_endpoint);
|
||||||
|
socket.send_to(asio::buffer(request_AAAA->data), mcast_endpoint);
|
||||||
|
|
||||||
|
|
||||||
|
// Should we care if this is called while already receiving? (async_receive call from receive_handler)
|
||||||
|
async_receive();
|
||||||
|
}
|
||||||
|
catch (std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveSocket::async_receive()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// our session to hold the buffer + endpoint
|
||||||
|
auto session = boost::make_shared<UdpSession>(this, replyfn);
|
||||||
|
socket.async_receive_from(asio::buffer(session->buffer, session->buffer.size())
|
||||||
|
, session->remote_endpoint
|
||||||
|
, boost::bind(&ResolveSocket::receive_handler, this, session, asio::placeholders::error, asio::placeholders::bytes_transferred));
|
||||||
|
}
|
||||||
|
catch (std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveSocket::receive_handler(SharedSession session, const error_code& error, size_t bytes)
|
||||||
|
{
|
||||||
|
// let io_service to handle the datagram on session
|
||||||
|
io_service->post(bind(&UdpSession::handle_receive, session, error, bytes));
|
||||||
|
// immediately accept new datagrams
|
||||||
|
async_receive();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UdpSession::handle_receive(const error_code& error, size_t bytes)
|
||||||
|
{
|
||||||
|
if (error) {
|
||||||
|
// todo: what level? do we even log? There might be callbacks when timer runs out
|
||||||
|
BOOST_LOG_TRIVIAL(info) << error.message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bytes == 0 || !replyfn) {
|
||||||
|
// todo: log something?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.resize(bytes);
|
||||||
|
#if 0
|
||||||
|
// this is log of buffer, be careful with logging here - called from async_receive
|
||||||
|
std::string str;
|
||||||
|
char const hex_chars[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
|
||||||
|
for (size_t i = 0; i < buffer.size(); i++) {
|
||||||
|
const char ch = buffer[i];
|
||||||
|
str += hex_chars[(ch & 0xF0) >> 4];
|
||||||
|
str += hex_chars[(ch & 0x0F) >> 0];
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(debug) << remote_endpoint.address()<< " " << str;
|
||||||
|
#endif
|
||||||
|
// decode buffer, txt keys are not needed for A / AAAA answer
|
||||||
|
auto dns_msg = DnsMessage::decode(buffer, Bonjour::TxtKeys());
|
||||||
|
if (dns_msg) {
|
||||||
|
asio::ip::address ip;
|
||||||
|
std::string answer_name;
|
||||||
|
if (dns_msg->rr_a) {
|
||||||
|
ip = dns_msg->rr_a->ip;
|
||||||
|
answer_name = dns_msg->rr_a->name;
|
||||||
|
}
|
||||||
|
else if (dns_msg->rr_aaaa) {
|
||||||
|
ip = dns_msg->rr_aaaa->ip;
|
||||||
|
answer_name = dns_msg->rr_aaaa->name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return; // not matching query type with answer type
|
||||||
|
|
||||||
|
if (!answer_name.empty()) {
|
||||||
|
// transform both strings to lower. Should we really do it?
|
||||||
|
std::string name_tolower = answer_name;
|
||||||
|
std::transform(name_tolower.begin(), name_tolower.end(), name_tolower.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
std::string hostname_tolower = socket->get_hostname();
|
||||||
|
std::transform(hostname_tolower.begin(), hostname_tolower.end(), hostname_tolower.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
if (name_tolower == hostname_tolower) {
|
||||||
|
BonjourReply reply(ip, 0, std::string(), answer_name, BonjourReply::TxtData());
|
||||||
|
replyfn(std::move(reply));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bonjour::priv::resolve_perform()
|
||||||
|
{
|
||||||
|
// reply callback is shared to every UDPSession which is called asyn
|
||||||
|
boost::mutex replies_guard;
|
||||||
|
std::vector<BonjourReply> replies;
|
||||||
|
// examples would probably store [self] to the lambda (and the timer one), is it ok not to do it? (Should be c++03)
|
||||||
|
const auto reply_callback = [&rpls = replies, &guard = replies_guard](BonjourReply&& reply)
|
||||||
|
{
|
||||||
|
guard.lock();
|
||||||
|
if (std::find(rpls.begin(), rpls.end(), reply) == rpls.end())
|
||||||
|
rpls.push_back(reply);
|
||||||
|
guard.unlock();
|
||||||
|
};
|
||||||
|
|
||||||
|
boost::shared_ptr< boost::asio::io_service > io_service(
|
||||||
|
new boost::asio::io_service
|
||||||
|
);
|
||||||
|
|
||||||
|
std::vector<ResolveSocket*> sockets;
|
||||||
|
|
||||||
|
// resolve intefaces - from PR#6646
|
||||||
|
std::vector<boost::asio::ip::address> interfaces;
|
||||||
|
asio::ip::udp::resolver resolver(*io_service);
|
||||||
|
// ipv4 interfaces
|
||||||
|
auto results = resolver.resolve(udp::v4(), asio::ip::host_name(), "");
|
||||||
|
for (auto const& r : results) {
|
||||||
|
auto const addr = r.endpoint().address();
|
||||||
|
if (addr.is_loopback()) continue;
|
||||||
|
interfaces.emplace_back(addr);
|
||||||
|
}
|
||||||
|
// create ipv4 socket for each interface
|
||||||
|
// each will send to querry to for both ipv4 and ipv6
|
||||||
|
for (const auto intrfc : interfaces)
|
||||||
|
sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, intrfc, io_service));
|
||||||
|
// ipv6 interfaces
|
||||||
|
interfaces.clear();
|
||||||
|
results = resolver.resolve(udp::v6(), asio::ip::host_name(), "");
|
||||||
|
for (auto const& r : results) {
|
||||||
|
auto const addr = r.endpoint().address();
|
||||||
|
if (addr.is_loopback()) continue;
|
||||||
|
interfaces.emplace_back(addr);
|
||||||
|
}
|
||||||
|
// create ipv6 socket for each interface
|
||||||
|
// each will send to querry to for both ipv4 and ipv6
|
||||||
|
for (const auto intrfc : interfaces)
|
||||||
|
sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, intrfc, io_service));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// send first queries
|
||||||
|
for each (auto * socket in sockets)
|
||||||
|
socket->send();
|
||||||
|
|
||||||
|
// timer settings
|
||||||
|
asio::deadline_timer timer(*io_service);
|
||||||
|
retries--;
|
||||||
|
std::function<void(const error_code&)> timer_handler = [&](const error_code& error) {
|
||||||
|
replies_guard.lock();
|
||||||
|
int replies_count = replies.size();
|
||||||
|
replies_guard.unlock();
|
||||||
|
// end
|
||||||
|
if (retries == 0 || error || replies_count > 0) {
|
||||||
|
// is this correct ending?
|
||||||
|
io_service->stop();
|
||||||
|
replies_guard.lock();
|
||||||
|
if (replies_count > 0 && resolvefn) {
|
||||||
|
resolvefn(replies);
|
||||||
|
}
|
||||||
|
replies_guard.unlock();
|
||||||
|
// restart timer
|
||||||
|
} else {
|
||||||
|
retries--;
|
||||||
|
timer.expires_from_now(boost::posix_time::seconds(timeout));
|
||||||
|
timer.async_wait(timer_handler);
|
||||||
|
// trigger another round of queries
|
||||||
|
for each (auto * socket in sockets)
|
||||||
|
socket->send();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// start timer
|
||||||
|
timer.expires_from_now(boost::posix_time::seconds(timeout));
|
||||||
|
timer.async_wait(timer_handler);
|
||||||
|
// start io_service, it will run until it has something to do - so in this case until stop is called in timer
|
||||||
|
io_service->run();
|
||||||
|
}
|
||||||
|
catch (std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// API - public part
|
// API - public part
|
||||||
|
|
||||||
@ -770,6 +1133,12 @@ Bonjour& Bonjour::set_timeout(unsigned timeout)
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bonjour& Bonjour::set_hostname(const std::string& hostname)
|
||||||
|
{
|
||||||
|
if (p) { p->hostname = hostname; }
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
Bonjour& Bonjour::set_retries(unsigned retries)
|
Bonjour& Bonjour::set_retries(unsigned retries)
|
||||||
{
|
{
|
||||||
if (p && retries > 0) { p->retries = retries; }
|
if (p && retries > 0) { p->retries = retries; }
|
||||||
@ -788,6 +1157,12 @@ Bonjour& Bonjour::on_complete(CompleteFn fn)
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bonjour& Bonjour::on_resolve(ResolveFn fn)
|
||||||
|
{
|
||||||
|
if (p) { p->resolvefn = std::move(fn); }
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
Bonjour::Ptr Bonjour::lookup()
|
Bonjour::Ptr Bonjour::lookup()
|
||||||
{
|
{
|
||||||
auto self = std::make_shared<Bonjour>(std::move(*this));
|
auto self = std::make_shared<Bonjour>(std::move(*this));
|
||||||
@ -803,4 +1178,26 @@ Bonjour::Ptr Bonjour::lookup()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Bonjour::Ptr Bonjour::resolve()
|
||||||
|
{
|
||||||
|
auto self = std::make_shared<Bonjour>(std::move(*this));
|
||||||
|
|
||||||
|
if (self->p) {
|
||||||
|
auto io_thread = std::thread([self]() {
|
||||||
|
self->p->resolve_perform();
|
||||||
|
});
|
||||||
|
self->p->io_thread = std::move(io_thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Bonjour::resolve_sync()
|
||||||
|
{
|
||||||
|
if (p)
|
||||||
|
p->resolve_perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ public:
|
|||||||
typedef std::shared_ptr<Bonjour> Ptr;
|
typedef std::shared_ptr<Bonjour> Ptr;
|
||||||
typedef std::function<void(BonjourReply &&)> ReplyFn;
|
typedef std::function<void(BonjourReply &&)> ReplyFn;
|
||||||
typedef std::function<void()> CompleteFn;
|
typedef std::function<void()> CompleteFn;
|
||||||
|
typedef std::function<void(const std::vector<BonjourReply>&)> ResolveFn;
|
||||||
typedef std::set<std::string> TxtKeys;
|
typedef std::set<std::string> TxtKeys;
|
||||||
|
|
||||||
Bonjour(std::string service);
|
Bonjour(std::string service);
|
||||||
@ -65,11 +66,22 @@ public:
|
|||||||
// ^ Note: By default there is 1 retry (meaning 1 broadcast is sent).
|
// ^ Note: By default there is 1 retry (meaning 1 broadcast is sent).
|
||||||
// Timeout is per one retry, ie. total time spent listening = retries * timeout.
|
// Timeout is per one retry, ie. total time spent listening = retries * timeout.
|
||||||
// If retries > 1, then care needs to be taken as more than one reply from the same service may be received.
|
// If retries > 1, then care needs to be taken as more than one reply from the same service may be received.
|
||||||
|
|
||||||
|
// sets hostname queried by resolve()
|
||||||
|
Bonjour& set_hostname(const std::string& hostname);
|
||||||
|
|
||||||
Bonjour& on_reply(ReplyFn fn);
|
Bonjour& on_reply(ReplyFn fn);
|
||||||
Bonjour& on_complete(CompleteFn fn);
|
Bonjour& on_complete(CompleteFn fn);
|
||||||
|
|
||||||
|
Bonjour& on_resolve(ResolveFn fn);
|
||||||
|
// lookup all devices by given TxtKeys
|
||||||
|
// each correct reply is passed back in ReplyFn, finishes with CompleteFn
|
||||||
Ptr lookup();
|
Ptr lookup();
|
||||||
|
// performs resolving of hostname into vector of ip adresses passed back by ResolveFn
|
||||||
|
// needs set_hostname and on_resolve to be called before.
|
||||||
|
Ptr resolve();
|
||||||
|
// resolve on the current thread
|
||||||
|
void resolve_sync();
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<priv> p;
|
std::unique_ptr<priv> p;
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include <boost/property_tree/ptree.hpp>
|
#include <boost/property_tree/ptree.hpp>
|
||||||
#include <boost/property_tree/json_parser.hpp>
|
#include <boost/property_tree/json_parser.hpp>
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
|
||||||
@ -18,7 +19,7 @@
|
|||||||
#include "slic3r/GUI/GUI.hpp"
|
#include "slic3r/GUI/GUI.hpp"
|
||||||
#include "Http.hpp"
|
#include "Http.hpp"
|
||||||
#include "libslic3r/AppConfig.hpp"
|
#include "libslic3r/AppConfig.hpp"
|
||||||
|
#include "Bonjour.hpp"
|
||||||
|
|
||||||
namespace fs = boost::filesystem;
|
namespace fs = boost::filesystem;
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
@ -103,11 +104,60 @@ OctoPrint::OctoPrint(DynamicPrintConfig *config) :
|
|||||||
|
|
||||||
const char* OctoPrint::get_name() const { return "OctoPrint"; }
|
const char* OctoPrint::get_name() const { return "OctoPrint"; }
|
||||||
|
|
||||||
bool OctoPrint::test(wxString &msg) const
|
bool OctoPrint::test_with_resolved_ip(wxString &msg) const
|
||||||
|
{
|
||||||
|
// Since the request is performed synchronously here,
|
||||||
|
// it is ok to refer to `msg` from within the closure
|
||||||
|
const char* name = get_name();
|
||||||
|
bool res = true;
|
||||||
|
// Msg contains ip string.
|
||||||
|
auto url = substitute_host(make_url("api/version"), GUI::into_u8(msg));
|
||||||
|
msg.Clear();
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
|
||||||
|
|
||||||
|
auto http = Http::get(std::move(url));
|
||||||
|
set_auth(http);
|
||||||
|
http
|
||||||
|
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
|
||||||
|
res = false;
|
||||||
|
msg = format_error(body, error, status);
|
||||||
|
})
|
||||||
|
.on_complete([&, this](std::string body, unsigned) {
|
||||||
|
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::stringstream ss(body);
|
||||||
|
pt::ptree ptree;
|
||||||
|
pt::read_json(ss, ptree);
|
||||||
|
|
||||||
|
if (!ptree.get_optional<std::string>("api")) {
|
||||||
|
res = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto text = ptree.get_optional<std::string>("text");
|
||||||
|
res = validate_version_text(text);
|
||||||
|
if (!res) {
|
||||||
|
msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "OctoPrint")).str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception&) {
|
||||||
|
res = false;
|
||||||
|
msg = "Could not parse server response.";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||||
|
.perform_sync();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OctoPrint::test(wxString& msg) const
|
||||||
{
|
{
|
||||||
// Since the request is performed synchronously here,
|
// Since the request is performed synchronously here,
|
||||||
// it is ok to refer to `msg` from within the closure
|
// it is ok to refer to `msg` from within the closure
|
||||||
|
|
||||||
const char *name = get_name();
|
const char *name = get_name();
|
||||||
|
|
||||||
bool res = true;
|
bool res = true;
|
||||||
@ -147,8 +197,8 @@ bool OctoPrint::test(wxString &msg) const
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
#ifdef WIN32
|
#ifdef WIN32
|
||||||
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||||
.on_ip_resolve([&](std::string address) {
|
.on_ip_resolve([&](std::string address) {
|
||||||
// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
|
// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
|
||||||
// Remember resolved address to be reused at successive REST API call.
|
// Remember resolved address to be reused at successive REST API call.
|
||||||
msg = GUI::from_u8(address);
|
msg = GUI::from_u8(address);
|
||||||
@ -159,6 +209,7 @@ bool OctoPrint::test(wxString &msg) const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
wxString OctoPrint::get_test_ok_msg () const
|
wxString OctoPrint::get_test_ok_msg () const
|
||||||
{
|
{
|
||||||
return _(L("Connection to OctoPrint works correctly."));
|
return _(L("Connection to OctoPrint works correctly."));
|
||||||
@ -174,7 +225,101 @@ wxString OctoPrint::get_test_failed_msg (wxString &msg) const
|
|||||||
|
|
||||||
bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
|
bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
|
||||||
{
|
{
|
||||||
const char *name = get_name();
|
#ifndef WIN32
|
||||||
|
return upload_inner_with_host(upload_data, prorgess_fn, error_fn);
|
||||||
|
#endif // !WIN32
|
||||||
|
|
||||||
|
// decide what to do based on m_host - resolve hostname or upload to ip
|
||||||
|
std::vector<boost::asio::ip::address> resolved_addr;
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::asio::ip::address host_ip = boost::asio::ip::make_address(m_host, ec);
|
||||||
|
if (!ec) {
|
||||||
|
resolved_addr.push_back(host_ip);
|
||||||
|
} else if ( GUI::get_app_config()->get("allow_ip_resolve") == "1"){
|
||||||
|
Bonjour("octoprint")
|
||||||
|
.set_hostname(m_host)
|
||||||
|
.set_retries(10) // number of rounds of queries send
|
||||||
|
.set_timeout(1) // after each timeout, if there is any answer, the resolving will stop
|
||||||
|
.on_resolve([&ra = resolved_addr](const std::vector<BonjourReply>& replies) {
|
||||||
|
std::vector<boost::asio::ip::address> resolved_addr;
|
||||||
|
for each (const auto & rpl in replies) {
|
||||||
|
boost::asio::ip::address ip(rpl.ip);
|
||||||
|
ra.emplace_back(ip);
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "Resolved IP address: " << rpl.ip;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.resolve_sync();
|
||||||
|
}
|
||||||
|
if (resolved_addr.empty()) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "PrusaSlicer failed to resolve hostname " << m_host << " into the IP address. Starting upload with system resolving.";
|
||||||
|
return false;//upload_inner_with_host(upload_data, prorgess_fn, error_fn);
|
||||||
|
}
|
||||||
|
return upload_inner(upload_data, prorgess_fn, error_fn, resolved_addr);
|
||||||
|
}
|
||||||
|
bool OctoPrint::upload_inner(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, const std::vector<boost::asio::ip::address>& resolved_addr) const
|
||||||
|
{
|
||||||
|
wxString error_message;
|
||||||
|
for each (const auto& ip in resolved_addr) {
|
||||||
|
// If test fails, test_msg_or_host_ip contains the error message.
|
||||||
|
// Otherwise on Windows it contains the resolved IP address of the host.
|
||||||
|
// Test_msg already contains resolved ip and will be cleared on start of test().
|
||||||
|
wxString test_msg_or_host_ip = GUI::from_u8(ip.to_string());
|
||||||
|
if (!test_with_resolved_ip(test_msg_or_host_ip)) {
|
||||||
|
error_message = test_msg_or_host_ip;
|
||||||
|
BOOST_LOG_TRIVIAL(info) << test_msg_or_host_ip;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* name = get_name();
|
||||||
|
const auto upload_filename = upload_data.upload_path.filename();
|
||||||
|
const auto upload_parent_path = upload_data.upload_path.parent_path();
|
||||||
|
std::string url = substitute_host(make_url("api/files/local"), ip.to_string());
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%")
|
||||||
|
% name
|
||||||
|
% upload_data.source_path
|
||||||
|
% url
|
||||||
|
% upload_filename.string()
|
||||||
|
% upload_parent_path.string()
|
||||||
|
% (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
|
||||||
|
|
||||||
|
auto http = Http::post(std::move(url));
|
||||||
|
set_auth(http);
|
||||||
|
http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
|
||||||
|
.form_add("path", upload_parent_path.string()) // XXX: slashes on windows ???
|
||||||
|
.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
|
||||||
|
.on_complete([&](std::string body, unsigned status) {
|
||||||
|
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body;
|
||||||
|
})
|
||||||
|
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
|
||||||
|
error_fn(format_error(body, error, status));
|
||||||
|
result = false;
|
||||||
|
})
|
||||||
|
.on_progress([&](Http::Progress progress, bool& cancel) {
|
||||||
|
prorgess_fn(std::move(progress), cancel);
|
||||||
|
if (cancel) {
|
||||||
|
// Upload was canceled
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "Octoprint: Upload canceled";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
#ifdef WIN32
|
||||||
|
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||||
|
#endif
|
||||||
|
.perform_sync();
|
||||||
|
if (result)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// todo: failed. Should we try again with host?
|
||||||
|
error_fn(std::move(error_message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OctoPrint::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
|
||||||
|
{
|
||||||
|
const char* name = get_name();
|
||||||
|
|
||||||
const auto upload_filename = upload_data.upload_path.filename();
|
const auto upload_filename = upload_data.upload_path.filename();
|
||||||
const auto upload_parent_path = upload_data.upload_path.parent_path();
|
const auto upload_parent_path = upload_data.upload_path.parent_path();
|
||||||
@ -182,7 +327,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro
|
|||||||
// If test fails, test_msg_or_host_ip contains the error message.
|
// If test fails, test_msg_or_host_ip contains the error message.
|
||||||
// Otherwise on Windows it contains the resolved IP address of the host.
|
// Otherwise on Windows it contains the resolved IP address of the host.
|
||||||
wxString test_msg_or_host_ip;
|
wxString test_msg_or_host_ip;
|
||||||
if (! test(test_msg_or_host_ip)) {
|
if (!test(test_msg_or_host_ip)) {
|
||||||
error_fn(std::move(test_msg_or_host_ip));
|
error_fn(std::move(test_msg_or_host_ip));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -233,7 +378,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro
|
|||||||
error_fn(format_error(body, error, status));
|
error_fn(format_error(body, error, status));
|
||||||
res = false;
|
res = false;
|
||||||
})
|
})
|
||||||
.on_progress([&](Http::Progress progress, bool &cancel) {
|
.on_progress([&](Http::Progress progress, bool& cancel) {
|
||||||
prorgess_fn(std::move(progress), cancel);
|
prorgess_fn(std::move(progress), cancel);
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
// Upload was canceled
|
// Upload was canceled
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include "PrintHost.hpp"
|
#include "PrintHost.hpp"
|
||||||
#include "libslic3r/PrintConfig.hpp"
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
|
|
||||||
|
class boost::asio::ip::address;
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
@ -35,6 +36,10 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual bool validate_version_text(const boost::optional<std::string> &version_text) const;
|
virtual bool validate_version_text(const boost::optional<std::string> &version_text) const;
|
||||||
|
virtual bool upload_inner(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, const std::vector<boost::asio::ip::address>& resolved_addr) const;
|
||||||
|
virtual bool test_with_resolved_ip(wxString& curl_msg) const;
|
||||||
|
virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const;
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_host;
|
std::string m_host;
|
||||||
|
Loading…
Reference in New Issue
Block a user