Skip to content
This repository was archived by the owner on Mar 29, 2026. It is now read-only.

Commit af4f89c

Browse files
committed
feat: Added heartbeats, fixed server being stuck in authentication, added some logging, rewrote unit test to properly test servers
1 parent edea9e5 commit af4f89c

6 files changed

Lines changed: 144 additions & 90 deletions

File tree

include/rconpp/server.h

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
namespace rconpp {
2424

2525
struct connected_client {
26-
struct sockaddr_in sock_info{};
26+
sockaddr_in sock_info{};
2727
int socket{0};
2828
bool connected{false};
2929

3030
bool authenticated{false};
31+
32+
time_t last_heartbeat{0};
3133
};
3234

3335
struct client_command {
@@ -47,9 +49,7 @@ class RCONPP_EXPORT rcon_server {
4749
#endif
4850

4951
std::thread accept_connections_runner;
50-
51-
// time_t is time since epoch in seconds (last time we ran).
52-
std::unordered_map<int, time_t> client_socket_to_last_heartbeat;
52+
std::mutex connected_clients_mutex;
5353

5454
public:
5555
bool online{false};
@@ -87,8 +87,9 @@ class RCONPP_EXPORT rcon_server {
8787
* @brief Disconnect a client from the server.
8888
*
8989
* @param client_socket The socket of the client to disconnect.
90+
* @param remove_after Should remove client from connected_clients after?
9091
*/
91-
void disconnect_client(const int client_socket);
92+
void disconnect_client(int client_socket, bool remove_after = true);
9293

9394
private:
9495

@@ -101,29 +102,21 @@ class RCONPP_EXPORT rcon_server {
101102
*/
102103
bool startup_server();
103104

104-
/**
105-
* @brief Ask to receive information from the server for a specified ID.
106-
*
107-
* @param id The ID that we should except the server to return, alongside information.
108-
* @param type The type of packet that we should expect.
109-
*
110-
* @return Data given by the server.
111-
*/
112-
response receive_information(int32_t id, data_type type);
113-
114105
/**
115106
* @brief Gathers all the packet's content (based on the length returned by `read_packet_length`)
116107
*
117108
* @param client Client to read packet from.
118109
*/
119-
void read_packet(rconpp::connected_client client);
110+
void read_packet(connected_client& client);
120111

121112
/**
122113
* @brief Sends a heartbeat to a client.
123114
*
124115
* @param client Client to send a heartbeat to.
116+
*
117+
* @returns bool, true is heartbeat was sent, otherwise false.
125118
*/
126-
void send_heartbeat(rconpp::connected_client client);
119+
bool send_heartbeat(connected_client& client);
127120
};
128121

129122
} // namespace rconpp

include/rconpp/utilities.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,6 @@ RCONPP_EXPORT void report_error();
101101
*
102102
* @return The size (not length) of the packet.
103103
*/
104-
RCONPP_EXPORT int read_packet_size(int socket, const std::function<void(const std::string_view log)>& on_log);
104+
RCONPP_EXPORT int read_packet_size(int socket);
105105

106106
} // namespace rconpp

src/rconpp/client.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ rconpp::rcon_client::rcon_client(const std::string_view addr, const int _port, c
66
}
77

88
rconpp::rcon_client::~rcon_client() {
9+
if (on_log) {
10+
on_log("RCON client is shutting down.");
11+
}
912
// Set connected to false, meaning no requests can be attempted during shutdown.
1013
connected = false;
1114

@@ -142,7 +145,12 @@ rconpp::response rconpp::rcon_client::receive_information(int32_t id, rconpp::da
142145
}
143146

144147
rconpp::packet rconpp::rcon_client::read_packet() {
145-
const int packet_size = read_packet_size(static_cast<int>(sock), on_log);
148+
const int packet_size = read_packet_size(static_cast<int>(sock));
149+
150+
if (packet_size == -1) {
151+
on_log("Did not receive a packet in time. Did the server send a response?");
152+
return {};
153+
}
146154

147155
packet temp_packet{};
148156
temp_packet.length = packet_size + 4;
@@ -205,7 +213,7 @@ void rconpp::rcon_client::start(const bool return_after) {
205213

206214
// The server will send SERVERDATA_AUTH_RESPONSE once it's happy. If it's not -1, the server will have accepted us!
207215
// We use the _sync method here to do a blocking call.
208-
const response response = send_data_sync(password, 1, data_type::SERVERDATA_AUTH, true);
216+
const response response = send_data_sync(password, 1, SERVERDATA_AUTH, true);
209217

210218
if (!response.server_responded) {
211219
on_log("Login data was incorrect. RCON++ will now abort.");

src/rconpp/server.cpp

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ rconpp::rcon_server::rcon_server(const std::string_view addr, const int _port, c
77
}
88

99
rconpp::rcon_server::~rcon_server() {
10+
if (on_log) {
11+
on_log("RCON server is shutting down.");
12+
}
13+
1014
// Set connected to false, meaning no requests can be attempted during shutdown.
1115
online = false;
1216

1317
terminating.notify_all();
1418

1519
// Safely disconnect all clients from server.
1620
for(const auto& client : connected_clients) {
17-
disconnect_client(client.first);
21+
disconnect_client(client.first, false);
1822
}
1923

2024
#ifdef _WIN32
@@ -26,10 +30,6 @@ rconpp::rcon_server::~rcon_server() {
2630
if (accept_connections_runner.joinable()) {
2731
accept_connections_runner.join();
2832
}
29-
30-
if (heartbeat_runner.joinable()) {
31-
heartbeat_runner.join();
32-
}
3333
}
3434

3535
bool rconpp::rcon_server::startup_server() {
@@ -84,27 +84,32 @@ bool rconpp::rcon_server::startup_server() {
8484
return true;
8585
}
8686

87-
void rconpp::rcon_server::disconnect_client(const int client_socket) {
87+
void rconpp::rcon_server::disconnect_client(const int client_socket, const bool remove_after /*= true*/) {
8888

8989
#ifdef _WIN32
9090
closesocket(client_socket);
9191
#else
9292
close(client_socket);
9393
#endif
9494

95+
std::lock_guard guard(connected_clients_mutex);
96+
9597
connected_clients.at(client_socket).connected = false;
9698

9799
if (request_handlers.at(client_socket).joinable()) {
98100
request_handlers.at(client_socket).join();
99101
}
100102

101-
connected_clients.erase(client_socket);
103+
if (remove_after) {
104+
connected_clients.erase(client_socket);
105+
}
102106
}
103107

104-
void rconpp::rcon_server::read_packet(rconpp::connected_client client) {
105-
const int packet_size = read_packet_size(static_cast<int>(sock), on_log);
108+
void rconpp::rcon_server::read_packet(connected_client& client) {
109+
const int packet_size = read_packet_size(static_cast<int>(client.socket));
106110

107-
if (packet_size <= MIN_PACKET_SIZE) {
111+
// Silently ignore packet size.
112+
if (packet_size < MIN_PACKET_SIZE) {
108113
return;
109114
}
110115

@@ -117,22 +122,26 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) {
117122
return;
118123
}
119124

125+
// Client is talking to us, we don't need to send a heartbeat if we're being talked to.
126+
client.last_heartbeat = time(nullptr);
127+
120128
std::string packet_data(&buffer[8], &buffer[buffer.size()-2]);
121129
int id = bit32_to_int(buffer);
122130
int type = type_to_int(buffer);
123131

124-
rconpp::packet packet_to_send{};
132+
packet packet_to_send{};
125133

126134
if (!client.authenticated) {
135+
on_log("Client not authenticated, handling authentication.");
127136
if (packet_data == password) {
128-
packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_AUTH_RESPONSE);
137+
packet_to_send = form_packet("", id, SERVERDATA_AUTH_RESPONSE);
129138
client.authenticated = true;
130139
} else {
131-
packet_to_send = form_packet("", -1, rconpp::data_type::SERVERDATA_AUTH_RESPONSE);
140+
packet_to_send = form_packet("", -1, SERVERDATA_AUTH_RESPONSE);
132141
}
133142
} else {
134-
if (type != rconpp::data_type::SERVERDATA_EXECCOMMAND) {
135-
packet_to_send = form_packet("Invalid packet type (" + std::to_string(type) + "). Double check your packets.", id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE);
143+
if (type != SERVERDATA_EXECCOMMAND) {
144+
packet_to_send = form_packet("Invalid packet type (" + std::to_string(type) + "). Double check your packets.", id, SERVERDATA_RESPONSE_VALUE);
136145
on_log("Invalid packet type (" + std::to_string(type) + ") sent by [" + inet_ntoa(client.sock_info.sin_addr) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]. Double check your packets.");
137146
} else {
138147
on_log("Client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "] has asked to execute the command: \"" + packet_data + "\"");
@@ -145,7 +154,7 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) {
145154
* It's better to just send no information and let clients assume that meant
146155
* the server didn't like the command.
147156
*/
148-
packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE);
157+
packet_to_send = form_packet("", id, SERVERDATA_RESPONSE_VALUE);
149158
} else {
150159
client_command command{};
151160
command.command = packet_data;
@@ -155,7 +164,7 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) {
155164

156165
on_log("Sending reply \"" + text_to_send + "\" to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "].");
157166

158-
packet_to_send = form_packet(text_to_send, id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE);
167+
packet_to_send = form_packet(text_to_send, id, SERVERDATA_RESPONSE_VALUE);
159168
}
160169
}
161170
}
@@ -169,6 +178,21 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) {
169178
}
170179
}
171180

181+
bool rconpp::rcon_server::send_heartbeat(connected_client& client) {
182+
on_log("Sending heartbeat to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]");
183+
184+
packet packet_to_send = form_packet("", -1, SERVERDATA_RESPONSE_VALUE);
185+
if (send(client.socket, packet_to_send.data.data(), packet_to_send.length, 0) < 0) {
186+
on_log("Failed to send a heartbeat to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]");
187+
report_error();
188+
return false;
189+
}
190+
191+
client.last_heartbeat = time(nullptr);
192+
193+
return true;
194+
}
195+
172196
void rconpp::rcon_server::start(bool return_after) {
173197
auto block_calling_thread = [this]() {
174198
std::mutex thread_mutex;
@@ -184,7 +208,7 @@ void rconpp::rcon_server::start(bool return_after) {
184208
on_log("Attempting to startup an RCON server...");
185209

186210
if (!startup_server()) {
187-
on_log("RCON++ is aborting as it failed to initiate server.");
211+
on_log("RCON server is aborting as it failed to initiate server.");
188212
return;
189213
}
190214

@@ -194,39 +218,37 @@ void rconpp::rcon_server::start(bool return_after) {
194218

195219
accept_connections_runner = std::thread([this]() {
196220
while (online) {
197-
connected_client client{};
198-
struct sockaddr_in client_info{};
221+
sockaddr_in client_info{};
199222

200223
socklen_t client_len = sizeof(client_info);
201224
int client_socket = accept(sock, reinterpret_cast<sockaddr*>(&client_info), &client_len);
202225

203226
if (client_socket == -1) {
204227
on_log("client with socket: \"" + std::to_string(client_socket) + "\" failed to connect.");
228+
report_error();
205229
continue;
206230
}
207231

208232
on_log("Client [" + std::string(inet_ntoa(client_info.sin_addr)) + ":" + std::to_string(ntohs(client_info.sin_port)) + "] has connected to the server.");
209233

234+
connected_client client{};
235+
210236
client.sock_info = client_info;
211237
client.socket = client_socket;
212238
client.connected = true;
213239

214-
std::thread client_thread([this, client]{
240+
std::thread client_thread([this, &client]{
215241
while (client.connected) {
216242
read_packet(client);
217243

218-
time_t current_time = time(nullptr);
219-
if (client_socket_to_last_heartbeat.find(client.socket) == client_socket_to_last_heartbeat.end()) {
220-
client_socket_to_last_heartbeat.insert({ client.socket, current_time });
221-
} else {
222-
time_t last_time = client_socket_to_last_heartbeat.at(client.socket);
244+
const time_t current_time = time(nullptr);
223245

224-
if (current_time - last_time >= HEARTBEAT_TIME)
246+
if (client.authenticated) {
247+
if (client.last_heartbeat == 0 || current_time - client.last_heartbeat >= HEARTBEAT_TIME)
225248
{
226-
send_heartbeat(client);
227-
228-
// We should check if the heartbeat actually got anything, if it does then insert back into the map.
229-
// if it failed, we bin that client off and shut this thread down.
249+
if (!send_heartbeat(client)) {
250+
disconnect_client(client.socket);
251+
}
230252
}
231253
}
232254

@@ -239,6 +261,8 @@ void rconpp::rcon_server::start(bool return_after) {
239261

240262
request_handlers.at(client_socket).detach();
241263

264+
std::lock_guard guard(connected_clients_mutex);
265+
242266
connected_clients.insert({ client_socket, client });
243267
}
244268
});

src/rconpp/utilities.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ void rconpp::report_error() {
4646
#endif
4747
}
4848

49-
int rconpp::read_packet_size(int socket, const std::function<void(const std::string_view log)>& on_log) {
49+
int rconpp::read_packet_size(int socket) {
5050
std::vector<char> buffer{};
5151
buffer.resize(4);
5252

@@ -55,7 +55,6 @@ int rconpp::read_packet_size(int socket, const std::function<void(const std::str
5555
* We simply just want to read that and then return it.
5656
*/
5757
if (recv(socket, buffer.data(), 4, 0) == -1) {
58-
on_log("Did not receive a packet in time. Did the server send a response?");
5958
report_error();
6059
return -1;
6160
}

0 commit comments

Comments
 (0)