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.
This commit is contained in:
@@ -185,6 +185,17 @@ void Packet::Send_string(const std::string_view data)
|
||||
this->buffer.emplace_back('\0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -366,6 +377,23 @@ uint64 Packet::Recv_uint64()
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@@ -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;
|
||||
|
@@ -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]);
|
||||
|
@@ -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);
|
||||
|
@@ -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.
|
||||
@@ -148,16 +188,29 @@ static CommandQueue _local_execution_queue;
|
||||
* @param text The text to pass
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
@@ -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 = "");
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user