summaryrefslogtreecommitdiff
path: root/src/network
diff options
context:
space:
mode:
authorMichael Lutz <michi@icosahedron.de>2021-10-28 23:48:26 +0200
committerMichael Lutz <michi@icosahedron.de>2021-12-16 22:28:32 +0100
commita05fd7aa50ecbee425df2d6f9015ec3ea359232f (patch)
tree9b9208d39813670789635b638592a903a2aafaa4 /src/network
parentb0990fcff7358e839468e5cf811ffddc8b9d73e2 (diff)
downloadopenttd-a05fd7aa50ecbee425df2d6f9015ec3ea359232f.tar.xz
Change: [Network] Transfer command data as serialized byte stream without fixed structure.
The data will be transmitted as the length followed by the serialized data. This allows the command data to be different for every command type in the future.
Diffstat (limited to 'src/network')
-rw-r--r--src/network/core/packet.cpp28
-rw-r--r--src/network/core/packet.h2
-rw-r--r--src/network/network.cpp28
-rw-r--r--src/network/network_admin.cpp5
-rw-r--r--src/network/network_command.cpp129
-rw-r--r--src/network/network_internal.h13
-rw-r--r--src/network/network_server.cpp8
7 files changed, 180 insertions, 33 deletions
diff --git a/src/network/core/packet.cpp b/src/network/core/packet.cpp
index e106d5787..ec0919757 100644
--- a/src/network/core/packet.cpp
+++ b/src/network/core/packet.cpp
@@ -186,6 +186,17 @@ void Packet::Send_string(const std::string_view data)
}
/**
+ * Copy a sized byte buffer into the packet.
+ * @param data The data to send.
+ */
+void Packet::Send_buffer(const std::vector<byte> &data)
+{
+ assert(this->CanWriteToPacket(sizeof(uint16) + data.size()));
+ this->Send_uint16((uint16)data.size());
+ this->buffer.insert(this->buffer.end(), data.begin(), data.end());
+}
+
+/**
* Send as many of the bytes as possible in the packet. This can mean
* that it is possible that not all bytes are sent. To cope with this
* the function returns the amount of bytes that were actually sent.
@@ -367,6 +378,23 @@ uint64 Packet::Recv_uint64()
}
/**
+ * Extract a sized byte buffer from the packet.
+ * @return The extracted buffer.
+ */
+std::vector<byte> Packet::Recv_buffer()
+{
+ uint16 size = this->Recv_uint16();
+ if (size == 0 || !this->CanReadFromPacket(size, true)) return {};
+
+ std::vector<byte> data;
+ while (size-- > 0) {
+ data.push_back(this->buffer[this->pos++]);
+ }
+
+ return data;
+}
+
+/**
* Reads characters (bytes) from the packet until it finds a '\0', or reaches a
* maximum of \c length characters.
* When the '\0' has not been reached in the first \c length read characters,
diff --git a/src/network/core/packet.h b/src/network/core/packet.h
index 277ff8bba..04a232e1c 100644
--- a/src/network/core/packet.h
+++ b/src/network/core/packet.h
@@ -72,6 +72,7 @@ public:
void Send_uint32(uint32 data);
void Send_uint64(uint64 data);
void Send_string(const std::string_view data);
+ void Send_buffer(const std::vector<byte> &data);
size_t Send_bytes (const byte *begin, const byte *end);
/* Reading/receiving of packets */
@@ -87,6 +88,7 @@ public:
uint16 Recv_uint16();
uint32 Recv_uint32();
uint64 Recv_uint64();
+ std::vector<byte> Recv_buffer();
std::string Recv_string(size_t length, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK);
size_t RemainingBytesToTransfer() const;
diff --git a/src/network/network.cpp b/src/network/network.cpp
index 13f2fe52a..8194f34d0 100644
--- a/src/network/network.cpp
+++ b/src/network/network.cpp
@@ -35,6 +35,7 @@
#include "../core/pool_func.hpp"
#include "../gfx_func.h"
#include "../error.h"
+#include "../misc_cmd.h"
#include <charconv>
#include <sstream>
#include <iomanip>
@@ -1064,8 +1065,8 @@ void NetworkGameLoop()
while (f != nullptr && !feof(f)) {
if (_date == next_date && _date_fract == next_date_fract) {
if (cp != nullptr) {
- NetworkSendCommand(cp->cmd, cp->err_msg, nullptr, cp->company, cp->tile, cp->p1, cp->p2, cp->text);
- Debug(desync, 0, "Injecting: {:08x}; {:02x}; {:02x}; {:06x}; {:08x}; {:08x}; {:08x}; \"{}\" ({})", _date, _date_fract, (int)_current_company, cp->tile, cp->p1, cp->p2, cp->cmd, cp->text, GetCommandName(cp->cmd));
+ NetworkSendCommand(cp->cmd, cp->err_msg, nullptr, cp->company, cp->data);
+ Debug(desync, 0, "Injecting: {:08x}; {:02x}; {:02x}; {:08x}; {:06x}; {} ({})", _date, _date_fract, (int)_current_company, cp->cmd, cp->tile, FormatArrayAsHex(cp->data), GetCommandName(cp->cmd));
delete cp;
cp = nullptr;
}
@@ -1104,15 +1105,21 @@ void NetworkGameLoop()
cp = new CommandPacket();
int company;
uint cmd;
- char buffer[128];
- int ret = sscanf(p, "%x; %x; %x; %x; %x; %x; %x; %x; \"%127[^\"]\"", &next_date, &next_date_fract, &company, &cp->tile, &cp->p1, &cp->p2, &cmd, &cp->err_msg, buffer);
- cp->text = buffer;
- /* There are 8 pieces of data to read, however the last is a
- * string that might or might not exist. Ignore it if that
- * string misses because in 99% of the time it's not used. */
- assert(ret == 9 || ret == 8);
+ char buffer[256];
+ int ret = sscanf(p, "%x; %x; %x; %x; %x; %x; %255s", &next_date, &next_date_fract, &company, &cmd, &cp->err_msg, &cp->tile, buffer);
+ assert(ret == 6);
cp->company = (CompanyID)company;
cp->cmd = (Commands)cmd;
+
+ /* Parse command data. */
+ std::vector<byte> args;
+ size_t arg_len = strlen(buffer);
+ for (size_t i = 0; i + 1 < arg_len; i += 2) {
+ byte e = 0;
+ std::from_chars(buffer + i, buffer + i + 1, e, 16);
+ args.emplace_back(e);
+ }
+ cp->data = args;
} else if (strncmp(p, "join: ", 6) == 0) {
/* Manually insert a pause when joining; this way the client can join at the exact right time. */
int ret = sscanf(p + 6, "%x; %x", &next_date, &next_date_fract);
@@ -1121,8 +1128,7 @@ void NetworkGameLoop()
cp = new CommandPacket();
cp->company = COMPANY_SPECTATOR;
cp->cmd = CMD_PAUSE;
- cp->p1 = PM_PAUSED_NORMAL;
- cp->p2 = 1;
+ cp->data = EndianBufferWriter<>::FromValue(CommandTraits<CMD_PAUSE>::Args{ 0, PM_PAUSED_NORMAL, 1, "" });
_ddc_fastforward = false;
} else if (strncmp(p, "sync: ", 6) == 0) {
int ret = sscanf(p + 6, "%x; %x; %x; %x", &next_date, &next_date_fract, &sync_state[0], &sync_state[1]);
diff --git a/src/network/network_admin.cpp b/src/network/network_admin.cpp
index 99f803e24..4711cdf04 100644
--- a/src/network/network_admin.cpp
+++ b/src/network/network_admin.cpp
@@ -630,10 +630,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendCmdLogging(ClientID clien
p->Send_uint32(client_id);
p->Send_uint8 (cp->company);
p->Send_uint16(cp->cmd);
- p->Send_uint32(cp->p1);
- p->Send_uint32(cp->p2);
- p->Send_uint32(cp->tile);
- p->Send_string(cp->text);
+ p->Send_buffer(cp->data);
p->Send_uint32(cp->frame);
this->SendPacket(p);
diff --git a/src/network/network_command.cpp b/src/network/network_command.cpp
index 0fae6bcbf..472d5e60e 100644
--- a/src/network/network_command.cpp
+++ b/src/network/network_command.cpp
@@ -15,18 +15,41 @@
#include "../company_func.h"
#include "../settings_type.h"
#include "../airport_cmd.h"
+#include "../aircraft_cmd.h"
+#include "../autoreplace_cmd.h"
+#include "../company_cmd.h"
#include "../depot_cmd.h"
#include "../dock_cmd.h"
+#include "../economy_cmd.h"
+#include "../engine_cmd.h"
+#include "../goal_cmd.h"
#include "../group_cmd.h"
#include "../industry_cmd.h"
+#include "../landscape_cmd.h"
+#include "../misc_cmd.h"
+#include "../news_cmd.h"
+#include "../object_cmd.h"
+#include "../order_cmd.h"
#include "../rail_cmd.h"
#include "../road_cmd.h"
+#include "../roadveh_cmd.h"
+#include "../settings_cmd.h"
+#include "../signs_cmd.h"
+#include "../station_cmd.h"
+#include "../story_cmd.h"
+#include "../subsidy_cmd.h"
#include "../terraform_cmd.h"
+#include "../timetable_cmd.h"
#include "../town_cmd.h"
#include "../train_cmd.h"
+#include "../tree_cmd.h"
#include "../tunnelbridge_cmd.h"
#include "../vehicle_cmd.h"
+#include "../viewport_cmd.h"
+#include "../water_cmd.h"
+#include "../waypoint_cmd.h"
#include "../script/script_cmd.h"
+#include <array>
#include "../safeguards.h"
@@ -62,6 +85,23 @@ static CommandCallback * const _callback_table[] = {
/* 0x1B */ CcAddVehicleNewGroup,
};
+/* Helpers to generate the command dispatch table from the command traits. */
+
+template <Commands Tcmd> static CommandDataBuffer SanitizeCmdStrings(const CommandDataBuffer &data);
+template <Commands Tcmd> static void UnpackNetworkCommand(const CommandPacket *cp);
+struct CommandDispatch {
+ CommandDataBuffer(*Sanitize)(const CommandDataBuffer &);
+ void (*Unpack)(const CommandPacket *);
+};
+
+template<typename T, T... i>
+inline constexpr auto MakeDispatchTable(std::integer_sequence<T, i...>) noexcept
+{
+ return std::array<CommandDispatch, sizeof...(i)>{{ { &SanitizeCmdStrings<static_cast<Commands>(i)>, &UnpackNetworkCommand<static_cast<Commands>(i)> }... }};
+}
+static constexpr auto _cmd_dispatch = MakeDispatchTable(std::make_integer_sequence<std::underlying_type_t<Commands>, CMD_END>{});
+
+
/**
* Append a CommandPacket at the end of the queue.
* @param p The packet to append to the queue.
@@ -149,15 +189,28 @@ static CommandQueue _local_execution_queue;
*/
void NetworkSendCommand(Commands cmd, StringID err_message, CommandCallback *callback, CompanyID company, TileIndex tile, uint32 p1, uint32 p2, const std::string &text)
{
+ auto data = EndianBufferWriter<CommandDataBuffer>::FromValue(std::make_tuple(tile, p1, p2, text));
+ NetworkSendCommand(cmd, err_message, callback, company, tile, data);
+}
+
+/**
+ * Prepare a DoCommand to be send over the network
+ * @param cmd The command to execute (a CMD_* value)
+ * @param err_message Message prefix to show on error
+ * @param callback A callback function to call after the command is finished
+ * @param company The company that wants to send the command
+ * @param location Location of the command (e.g. for error message position)
+ * @param cmd_data The command proc arguments.
+ */
+void NetworkSendCommand(Commands cmd, StringID err_message, CommandCallback *callback, CompanyID company, TileIndex location, const CommandDataBuffer &cmd_data)
+{
CommandPacket c;
c.company = company;
- c.tile = tile;
- c.p1 = p1;
- c.p2 = p2;
c.cmd = cmd;
c.err_msg = err_message;
c.callback = callback;
- c.text = text;
+ c.tile = location;
+ c.data = cmd_data;
if (_network_server) {
/* If we are the server, we queue the command in our 'special' queue.
@@ -220,7 +273,7 @@ void NetworkExecuteLocalCommandQueue()
/* We can execute this command */
_current_company = cp->company;
- DoCommandP(cp, cp->my_cmd, true);
+ _cmd_dispatch[cp->cmd].Unpack(cp);
queue.Pop();
delete cp;
@@ -311,11 +364,8 @@ const char *NetworkGameSocketHandler::ReceiveCommand(Packet *p, CommandPacket *c
if (!IsValidCommand(cp->cmd)) return "invalid command";
if (GetCommandFlags(cp->cmd) & CMD_OFFLINE) return "single-player only command";
cp->err_msg = p->Recv_uint16();
-
- cp->p1 = p->Recv_uint32();
- cp->p2 = p->Recv_uint32();
cp->tile = p->Recv_uint32();
- cp->text = p->Recv_string(NETWORK_COMPANY_NAME_LENGTH, (!_network_server && GetCommandFlags(cp->cmd) & CMD_STR_CTRL) != 0 ? SVS_ALLOW_CONTROL_CODE | SVS_REPLACE_WITH_QUESTION_MARK : SVS_REPLACE_WITH_QUESTION_MARK);
+ cp->data = _cmd_dispatch[cp->cmd].Sanitize(p->Recv_buffer());
byte callback = p->Recv_uint8();
if (callback >= lengthof(_callback_table)) return "invalid callback";
@@ -331,13 +381,11 @@ const char *NetworkGameSocketHandler::ReceiveCommand(Packet *p, CommandPacket *c
*/
void NetworkGameSocketHandler::SendCommand(Packet *p, const CommandPacket *cp)
{
- p->Send_uint8 (cp->company);
+ p->Send_uint8(cp->company);
p->Send_uint16(cp->cmd);
p->Send_uint16(cp->err_msg);
- p->Send_uint32(cp->p1);
- p->Send_uint32(cp->p2);
p->Send_uint32(cp->tile);
- p->Send_string(cp->text);
+ p->Send_buffer(cp->data);
byte callback = 0;
while (callback < lengthof(_callback_table) && _callback_table[callback] != cp->callback) {
@@ -350,3 +398,58 @@ void NetworkGameSocketHandler::SendCommand(Packet *p, const CommandPacket *cp)
}
p->Send_uint8 (callback);
}
+
+/**
+ * Insert a client ID into the command data in a command packet.
+ * @param cp Command packet to modify.
+ * @param client_id Client id to insert.
+ */
+void NetworkReplaceCommandClientId(CommandPacket &cp, ClientID client_id)
+{
+ /* Unpack command parameters. */
+ auto params = EndianBufferReader::ToValue<std::tuple<TileIndex, uint32, uint32, std::string>>(cp.data);
+
+ /* Insert client id. */
+ std::get<2>(params) = client_id;
+
+ /* Repack command parameters. */
+ cp.data = EndianBufferWriter<CommandDataBuffer>::FromValue(params);
+}
+
+
+/** Validate a single string argument coming from network. */
+template <class T>
+static inline void SanitizeSingleStringHelper([[maybe_unused]] CommandFlags cmd_flags, T &data)
+{
+ if constexpr (std::is_same_v<std::string, T>) {
+ data = StrMakeValid(data.substr(0, NETWORK_COMPANY_NAME_LENGTH), (!_network_server && cmd_flags & CMD_STR_CTRL) != 0 ? SVS_ALLOW_CONTROL_CODE | SVS_REPLACE_WITH_QUESTION_MARK : SVS_REPLACE_WITH_QUESTION_MARK);
+ }
+}
+
+/** Helper function to perform validation on command data strings. */
+template<class Ttuple, size_t... Tindices>
+static inline void SanitizeStringsHelper(CommandFlags cmd_flags, Ttuple &values, std::index_sequence<Tindices...>)
+{
+ ((SanitizeSingleStringHelper(cmd_flags, std::get<Tindices>(values))), ...);
+}
+
+/**
+ * Validate and sanitize strings in command data.
+ * @tparam Tcmd Command this data belongs to.
+ * @param data Command data.
+ * @return Sanitized command data.
+ */
+template <Commands Tcmd>
+CommandDataBuffer SanitizeCmdStrings(const CommandDataBuffer &data)
+{
+ auto args = EndianBufferReader::ToValue<typename CommandTraits<Tcmd>::Args>(data);
+ SanitizeStringsHelper(CommandTraits<Tcmd>::flags, args, std::make_index_sequence<std::tuple_size_v<typename CommandTraits<Tcmd>::Args>>{});
+ return EndianBufferWriter<CommandDataBuffer>::FromValue(args);
+}
+
+template <Commands Tcmd>
+void UnpackNetworkCommand(const CommandPacket *cp)
+{
+ auto args = EndianBufferReader::ToValue<typename CommandTraits<Tcmd>::Args>(cp->data);
+ std::apply(&InjectNetworkCommand, std::tuple_cat(std::make_tuple(Tcmd, cp->err_msg, cp->callback, cp->my_cmd), args));
+}
diff --git a/src/network/network_internal.h b/src/network/network_internal.h
index 25240da5d..58c99867c 100644
--- a/src/network/network_internal.h
+++ b/src/network/network_internal.h
@@ -15,6 +15,8 @@
#include "core/tcp_game.h"
#include "../command_type.h"
+#include "../command_func.h"
+#include "../misc/endian_buffer.hpp"
#ifdef RANDOM_DEBUG
/**
@@ -104,19 +106,26 @@ void UpdateNetworkGameWindow();
/**
* Everything we need to know about a command to be able to execute it.
*/
-struct CommandPacket : CommandContainer {
+struct CommandPacket {
/** Make sure the pointer is nullptr. */
- CommandPacket() : next(nullptr), company(INVALID_COMPANY), frame(0), my_cmd(false) {}
+ CommandPacket() : next(nullptr), company(INVALID_COMPANY), frame(0), my_cmd(false), tile(0) {}
CommandPacket *next; ///< the next command packet (if in queue)
CompanyID company; ///< company that is executing the command
uint32 frame; ///< the frame in which this packet is executed
bool my_cmd; ///< did the command originate from "me"
+
+ Commands cmd; ///< command being executed.
+ StringID err_msg; ///< string ID of error message to use.
+ CommandCallback *callback; ///< any callback function executed upon successful completion of the command.
+ TileIndex tile; ///< location of the command (for e.g. error message or effect display).
+ CommandDataBuffer data; ///< command parameters.
};
void NetworkDistributeCommands();
void NetworkExecuteLocalCommandQueue();
void NetworkFreeLocalCommandQueue();
void NetworkSyncCommandQueue(NetworkClientSocket *cs);
+void NetworkReplaceCommandClientId(CommandPacket &cp, ClientID client_id);
void ShowNetworkError(StringID error_string);
void NetworkTextMessage(NetworkAction action, TextColour colour, bool self_send, const std::string &name, const std::string &str = "", int64 data = 0, const std::string &data_str = "");
diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp
index 50b46dfe1..967ad40a8 100644
--- a/src/network/network_server.cpp
+++ b/src/network/network_server.cpp
@@ -24,6 +24,7 @@
#include "../genworld.h"
#include "../company_func.h"
#include "../company_gui.h"
+#include "../company_cmd.h"
#include "../roadveh.h"
#include "../order_backup.h"
#include "../core/pool_func.hpp"
@@ -1048,14 +1049,15 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_COMMAND(Packet
* to match the company in the packet. If it doesn't, the client has done
* something pretty naughty (or a bug), and will be kicked
*/
- if (!(cp.cmd == CMD_COMPANY_CTRL && cp.p1 == 0 && ci->client_playas == COMPANY_NEW_COMPANY) && ci->client_playas != cp.company) {
+ uint32 company_p1 = cp.cmd == CMD_COMPANY_CTRL ? std::get<1>(EndianBufferReader::ToValue<CommandTraits<CMD_COMPANY_CTRL>::Args>(cp.data)) : 0;
+ if (!(cp.cmd == CMD_COMPANY_CTRL && company_p1 == 0 && ci->client_playas == COMPANY_NEW_COMPANY) && ci->client_playas != cp.company) {
IConsolePrint(CC_WARNING, "Kicking client #{} (IP: {}) due to calling a command as another company {}.",
ci->client_playas + 1, this->GetClientIP(), cp.company + 1);
return this->SendError(NETWORK_ERROR_COMPANY_MISMATCH);
}
if (cp.cmd == CMD_COMPANY_CTRL) {
- if (cp.p1 != 0 || cp.company != COMPANY_SPECTATOR) {
+ if (company_p1 != 0 || cp.company != COMPANY_SPECTATOR) {
return this->SendError(NETWORK_ERROR_CHEATER);
}
@@ -1066,7 +1068,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_COMMAND(Packet
}
}
- if (GetCommandFlags(cp.cmd) & CMD_CLIENT_ID) cp.p2 = this->client_id;
+ if (GetCommandFlags(cp.cmd) & CMD_CLIENT_ID) NetworkReplaceCommandClientId(cp, this->client_id);
this->incoming_queue.Append(&cp);
return NETWORK_RECV_STATUS_OKAY;