Move all the connect() logic from TCPSocketHandler into a subclass

This way, TCPSocketHandler only deal with the message sending/receiving, not
the connect() or anything else.  This will be used for implementing servers
(because when a client is accepted, we don’t need all the connect() and dns
resolution stuff).
This commit is contained in:
louiz’
2016-11-11 02:54:48 +01:00
parent c41d003cbf
commit 0c8adc85f7
11 changed files with 357 additions and 300 deletions
+18 -216
View File
@@ -1,8 +1,6 @@
#include <network/tcp_socket_handler.hpp>
#include <network/dns_handler.hpp>
#include <utils/timed_events.hpp>
#include <utils/scopeguard.hpp>
#include <network/poller.hpp>
#include <logger/logger.hpp>
@@ -12,7 +10,6 @@
#include <unistd.h>
#include <errno.h>
#include <cstring>
#include <fcntl.h>
#ifdef BOTAN_FOUND
# include <botan/hex.h>
@@ -35,10 +32,7 @@ namespace ph = std::placeholders;
TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller> poller):
SocketHandler(poller, -1),
use_tls(false),
connected(false),
connecting(false),
hostname_resolution_failed(false)
use_tls(false)
#ifdef BOTAN_FOUND
,credential_manager(this)
#endif
@@ -46,181 +40,13 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller> poller):
TCPSocketHandler::~TCPSocketHandler()
{
this->close();
}
void TCPSocketHandler::init_socket(const struct addrinfo* rp)
{
if (this->poller->is_managing_socket(this->get_socket()))
this->poller->remove_socket_handler(this->get_socket());
if (this->socket != -1)
::close(this->socket);
if ((this->socket = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
throw std::runtime_error("Could not create socket: "s + strerror(errno));
// Bind the socket to a specific address, if specified
if (!this->bind_addr.empty())
{
// Convert the address from string format to a sockaddr that can be
// used in bind()
struct addrinfo* result;
int err = ::getaddrinfo(this->bind_addr.data(), nullptr, nullptr, &result);
if (err != 0 || !result)
log_error("Failed to bind socket to ", this->bind_addr, ": ",
gai_strerror(err));
else
{
utils::ScopeGuard sg([result](){ freeaddrinfo(result); });
struct addrinfo* rp;
int bind_error = 0;
for (rp = result; rp; rp = rp->ai_next)
{
if ((bind_error = ::bind(this->socket,
reinterpret_cast<const struct sockaddr*>(rp->ai_addr),
rp->ai_addrlen)) == 0)
break;
}
if (!rp)
log_error("Failed to bind socket to ", this->bind_addr, ": ",
strerror(errno));
else
log_info("Socket successfully bound to ", this->bind_addr);
}
::close(this->socket);
this->socket = -1;
}
int optval = 1;
if (::setsockopt(this->socket, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1)
log_warning("Failed to enable TCP keepalive on socket: ", strerror(errno));
// Set the socket on non-blocking mode. This is useful to receive a EAGAIN
// error when connect() would block, to not block the whole process if a
// remote is not responsive.
const int existing_flags = ::fcntl(this->socket, F_GETFL, 0);
if ((existing_flags == -1) ||
(::fcntl(this->socket, F_SETFL, existing_flags | O_NONBLOCK) == -1))
throw std::runtime_error("Could not initialize socket: "s + strerror(errno));
}
void TCPSocketHandler::connect(const std::string& address, const std::string& port, const bool tls)
{
this->address = address;
this->port = port;
this->use_tls = tls;
struct addrinfo* addr_res;
if (!this->connecting)
{
// Get the addrinfo from getaddrinfo (or ares_gethostbyname), only if
// this is the first call of this function.
if (!this->resolver.is_resolved())
{
log_info("Trying to connect to ", address, ":", port);
// Start the asynchronous process of resolving the hostname. Once
// the addresses have been found and `resolved` has been set to true
// (but connecting will still be false), TCPSocketHandler::connect()
// needs to be called, again.
this->resolver.resolve(address, port,
[this](const struct addrinfo*)
{
log_debug("Resolution success, calling connect() again");
this->connect();
},
[this](const char*)
{
log_debug("Resolution failed, calling connect() again");
this->connect();
});
return;
}
else
{
// The c-ares resolved the hostname and the available addresses
// where saved in the cares_addrinfo linked list. Now, just use
// this list to try to connect.
addr_res = this->resolver.get_result().get();
if (!addr_res)
{
this->hostname_resolution_failed = true;
const auto msg = this->resolver.get_error_message();
this->close();
this->on_connection_failed(msg);
return ;
}
}
}
else
{ // This function is called again, use the saved addrinfo structure,
// instead of re-doing the whole getaddrinfo process.
addr_res = &this->addrinfo;
}
for (struct addrinfo* rp = addr_res; rp; rp = rp->ai_next)
{
if (!this->connecting)
{
try {
this->init_socket(rp);
}
catch (const std::runtime_error& error) {
log_error("Failed to init socket: ", error.what());
break;
}
}
this->display_resolved_ip(rp);
if (::connect(this->socket, rp->ai_addr, rp->ai_addrlen) == 0
|| errno == EISCONN)
{
log_info("Connection success.");
TimedEventsManager::instance().cancel("connection_timeout"s +
std::to_string(this->socket));
this->poller->add_socket_handler(this);
this->connected = true;
this->connecting = false;
#ifdef BOTAN_FOUND
if (this->use_tls)
this->start_tls();
#endif
this->connection_date = std::chrono::system_clock::now();
this->on_connected();
return ;
}
else if (errno == EINPROGRESS || errno == EALREADY)
{ // retry this process later, when the socket
// is ready to be written on.
this->connecting = true;
this->poller->add_socket_handler(this);
this->poller->watch_send_events(this);
// Save the addrinfo structure, to use it on the next call
this->ai_addrlen = rp->ai_addrlen;
memcpy(&this->ai_addr, rp->ai_addr, this->ai_addrlen);
memcpy(&this->addrinfo, rp, sizeof(struct addrinfo));
this->addrinfo.ai_addr = reinterpret_cast<struct sockaddr*>(&this->ai_addr);
this->addrinfo.ai_next = nullptr;
// If the connection has not succeeded or failed in 5s, we consider
// it to have failed
TimedEventsManager::instance().add_event(
TimedEvent(std::chrono::steady_clock::now() + 5s,
std::bind(&TCPSocketHandler::on_connection_timeout, this),
"connection_timeout"s + std::to_string(this->socket)));
return ;
}
log_info("Connection failed:", strerror(errno));
}
log_error("All connection attempts failed.");
this->close();
this->on_connection_failed(strerror(errno));
return ;
}
void TCPSocketHandler::on_connection_timeout()
{
this->close();
this->on_connection_failed("connection timed out");
}
void TCPSocketHandler::connect()
{
this->connect(this->address, this->port, this->use_tls);
}
void TCPSocketHandler::on_recv()
@@ -267,13 +93,13 @@ ssize_t TCPSocketHandler::do_recv(void* recv_buf, const size_t buf_size)
}
else if (-1 == size)
{
if (this->connecting)
if (this->is_connecting())
log_warning("Error connecting: ", strerror(errno));
else
log_warning("Error while reading from socket: ", strerror(errno));
// Remember if we were connecting, or already connected when this
// happened, because close() sets this->connecting to false
const auto were_connecting = this->connecting;
const auto were_connecting = this->is_connecting();
this->close();
if (were_connecting)
this->on_connection_failed(strerror(errno));
@@ -333,29 +159,15 @@ void TCPSocketHandler::on_send()
void TCPSocketHandler::close()
{
TimedEventsManager::instance().cancel("connection_timeout"s +
std::to_string(this->socket));
if (this->connected || this->connecting)
if (this->is_connected() || this->is_connecting())
this->poller->remove_socket_handler(this->get_socket());
if (this->socket != -1)
{
::close(this->socket);
this->socket = -1;
}
this->connected = false;
this->connecting = false;
this->in_buf.clear();
this->out_buf.clear();
this->port.clear();
this->resolver.clear();
}
void TCPSocketHandler::display_resolved_ip(struct addrinfo* rp) const
{
if (rp->ai_family == AF_INET)
log_debug("Trying IPv4 address ", addr_to_string(rp));
else if (rp->ai_family == AF_INET6)
log_debug("Trying IPv6 address ", addr_to_string(rp));
}
void TCPSocketHandler::send_data(std::string&& data)
@@ -379,45 +191,35 @@ void TCPSocketHandler::raw_send(std::string&& data)
if (data.empty())
return ;
this->out_buf.emplace_back(std::move(data));
if (this->connected)
if (this->is_connected())
this->poller->watch_send_events(this);
}
void TCPSocketHandler::send_pending_data()
{
if (this->connected && !this->out_buf.empty())
if (this->is_connected() && !this->out_buf.empty())
this->poller->watch_send_events(this);
}
bool TCPSocketHandler::is_connected() const
{
return this->connected;
}
bool TCPSocketHandler::is_connecting() const
{
return this->connecting || this->resolver.is_resolving();
}
bool TCPSocketHandler::is_using_tls() const
{
return this->use_tls;
}
std::string TCPSocketHandler::get_port() const
{
return this->port;
}
void* TCPSocketHandler::get_receive_buffer(const size_t) const
{
return nullptr;
}
#ifdef BOTAN_FOUND
void TCPSocketHandler::start_tls()
void TCPSocketHandler::consume_in_buffer(const std::size_t size)
{
Botan::TLS::Server_Information server_info(this->address, "irc", std::stoul(this->port));
this->in_buf = this->in_buf.substr(size, std::string::npos);
}
#ifdef BOTAN_FOUND
void TCPSocketHandler::start_tls(const std::string& address, const std::string& port)
{
Botan::TLS::Server_Information server_info(address, "irc", std::stoul(port));
this->tls = std::make_unique<Botan::TLS::Client>(
std::bind(&TCPSocketHandler::tls_output_fn, this, ph::_1, ph::_2),
std::bind(&TCPSocketHandler::tls_data_cb, this, ph::_1, ph::_2),