13 Commits

Author SHA1 Message Date
b1b46ca08c Implement platform specific input buffer flushing 2024-12-06 21:53:39 +02:00
2bb1fafdaa Add hardening flags 2024-12-06 21:53:14 +02:00
0fb17971c0 Fix typo and remove 'Q' as quitting command 2024-12-06 20:26:42 +02:00
ec11d77e89 Update CHANGELOG.md 2024-12-06 16:32:11 +02:00
2d29741691 Fixes to Windows utf-8 support + one more Minecraft newline fix 2024-12-06 16:24:04 +02:00
cc77044df1 Add experimental utf-8 support for Windows and change the behaviour of Minecraft "stop" hack 2024-12-06 14:43:40 +02:00
dba07aacf7 Update CHANGELOG.md 2024-12-06 12:35:30 +02:00
bd76b897de Fix missing newlines in Minecraft RCON messages:
Minecraft servers have a longstanding bug that omits newlines
when sending messages via RCON. This patch manually inserts
newlines to address the issue.

Reference: https://bugs.mojang.com/browse/MC-7569

Fixes #1
2024-12-06 11:00:18 +02:00
1106f27700 Bump version to 0.8.0 2024-12-06 08:47:45 +02:00
00fc3b5bcb Set select() timeout to 5 seconds and replace putchar() loop with fputs() call 2024-12-06 08:47:45 +02:00
a0fe9e1645 Enable input/output buffering and fflush only explicitly
As suggested in old pull request: #39
2024-12-06 08:47:45 +02:00
5f460e8912 Remove Valve protocol checks and set select() timeout to 1.5 seconds
Use the same method for handling multipacket responses across all servers.
2024-12-06 08:47:45 +02:00
6fed74ba74 Implement select() loop to receive all incoming packets:
Send a "multipacket guard" - an empty packet with an invalid 'cmd' field
and a unique packet ID to trigger a reply from the server once the previous
command's reply has been fully sent.

Valve returns an empty payload, while Minecraft includes an error message in the payload.

This workaround ensures that all packets related to the last valid command
are received from the server, avoiding the need to wait for the select() timeout.
2024-12-06 08:47:45 +02:00
4 changed files with 248 additions and 103 deletions

View File

@ -1,17 +1,21 @@
#### Version history:
###### 0.7.3
- Add support to Valve style rcon authentication
###### 0.8.0
- Implement support for multipacket responses
- Add support for Valve style rcon authentication
- Add experimental UTF-8 support for Windows
- Change maximum packet size to correct value (4096 -> 4106)
- Attempt to add missing newlines in bugged Minecraft servers
* Implemented for responses to the 'help' command and unknown commands
- Print auth failed message to stderr instead of stdout
- Fail immediately if received packet size is out of spec
- Return proper exit code from run_terminal_mode()
- Add error messages to rcon_command() function
###### 0.7.2
- Quit gracefully when Ctrl-D or Ctrl+C is pressed
- Remove "exit" and "quit" as quitting commands
* these are actual rcon commands on some servers
* These are actual rcon commands on some servers
- Suppress compiler warning (strncpy)
- Fix erroneous string length in packet building function
- Fix typo in ANSI escape sequence for LCYAN

View File

@ -15,7 +15,7 @@ RM = rm -v -f
CC ?= gcc
CFLAGS = -std=gnu99 -Wall -Wextra -Wpedantic -Wno-gnu-zero-variadic-macro-arguments -O2
EXTRAFLAGS ?= -fstack-protector-all
EXTRAFLAGS ?= -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pie -Wl,-z,relro -Wl,-z,now -fno-common
ifeq ($(OS), Windows_NT)
LINKER = -lws2_32

View File

@ -1,7 +1,7 @@
.\" Process this file with
.\" groff -man -Tascii mcrcon.1
.\"
.TH MCRCON 1 "November 2024" "Version 0.7.3"
.TH MCRCON 1 "December 2024" "Version 0.8.0"
.SH NAME
mcrcon \- send rcon commands to a Minecraft server
.SH SYNOPSIS

335
mcrcon.c
View File

@ -31,17 +31,20 @@
#include <errno.h>
#include <unistd.h>
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <fcntl.h>
#include <wchar.h>
#else
#include <sys/socket.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netdb.h>
#include <termios.h>
#endif
#define VERSION "0.7.3"
#define VERSION "0.8.0"
#define IN_NAME "mcrcon"
#define VER_STR IN_NAME" "VERSION" (built: "__DATE__" "__TIME__")"
@ -56,19 +59,21 @@
#define MAX_PACKET_SIZE 4106 // id (4) + cmd (4) + DATA_BUFFSIZE
#define MIN_PACKET_SIZE 10 // id (4) + cmd (4) + two empty strings (2)
#define MAX_WAIT_TIME 600
#define log_error(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__);
// rcon packet structure
typedef struct {
int32_t size;
int32_t id;
int32_t cmd;
uint8_t data[DATA_BUFFSIZE];
// ignoring string2 for now
int32_t size;
int32_t id;
int32_t cmd;
uint8_t data[DATA_BUFFSIZE];
// ignoring string2 for now
} rc_packet;
// ===================================
// FUNCTION DEFINITIONS
// FUNCTION DEFINITIONS
// ===================================
// Network related functions
@ -95,20 +100,25 @@ void packet_print(rc_packet *packet);
bool rcon_auth(int sock, char *passwd);
int rcon_command(int sock, char *command);
// =============================================
// GLOBAL VARIABLES
// =============================================
static int flag_raw_output = 0;
static int flag_silent_mode = 0;
static int flag_disable_colors = 0;
static int flag_wait_seconds = 0;
static int global_connection_alive = 1;
static int global_rsock;
static int flag_raw_output = 0;
static int flag_silent_mode = 0;
static int flag_disable_colors = 0;
static int flag_wait_seconds = 0;
static int global_connection_alive = 1;
static bool global_valve_protocol = false;
static bool global_minecraft_newline_fix = false;
static int global_rsock;
#ifdef _WIN32
// console coloring on windows
HANDLE console_handle;
// console coloring on windows
HANDLE console_handle;
// console code pages
UINT old_output_codepage;
UINT old_input_codepage;
#endif
// safety stuff (windows is still misbehaving)
@ -116,6 +126,15 @@ void exit_proc(void)
{
if (global_rsock != -1)
net_close(global_rsock);
#ifdef _WIN32
// Restore previous code pages
SetConsoleOutputCP(old_output_codepage);
SetConsoleCP(old_input_codepage);
// Set back to binary mode
_setmode(_fileno(stdin), _O_BINARY);
#endif
}
// TODO: check exact windows and linux behaviour
@ -125,13 +144,9 @@ void sighandler(int sig)
putchar('\n');
global_connection_alive = 0;
#ifndef _WIN32
exit(EXIT_SUCCESS);
#endif
exit(EXIT_SUCCESS);
}
#define MAX_WAIT_TIME 600
unsigned int mcrcon_parse_seconds(char *str)
{
char *end;
@ -162,14 +177,10 @@ int main(int argc, char *argv[])
char *host = getenv("MCRCON_HOST");
char *pass = getenv("MCRCON_PASS");
char *port = getenv("MCRCON_PORT");
if (!port) port = "25575";
if (!host) host = "localhost";
// disable output buffering (https://github.com/Tiiffi/mcrcon/pull/39)
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
if(argc < 1 && pass == NULL) usage();
// default getopt error handler enabled
@ -188,7 +199,7 @@ int main(int argc, char *argv[])
case 'r': flag_raw_output = 1; break;
case 'w':
flag_wait_seconds = mcrcon_parse_seconds(optarg);
break;
break;
case 'v':
puts(VER_STR);
@ -219,10 +230,20 @@ int main(int argc, char *argv[])
signal(SIGINT, &sighandler);
#ifdef _WIN32
net_init_WSA();
console_handle = GetStdHandle(STD_OUTPUT_HANDLE);
if (console_handle == INVALID_HANDLE_VALUE)
console_handle = NULL;
net_init_WSA();
console_handle = GetStdHandle(STD_OUTPUT_HANDLE);
if (console_handle == INVALID_HANDLE_VALUE)
console_handle = NULL;
// Set the output and input code pages to utf-8
old_output_codepage = GetConsoleOutputCP();
old_input_codepage = GetConsoleCP();
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
// Set the file translation mode to UTF16
_setmode(_fileno(stdin), _O_U16TEXT);
#endif
// open socket
@ -273,8 +294,8 @@ void usage(void)
puts("Example:\n\t"IN_NAME" -H my.minecraft.server -p password -w 5 \"say Server is restarting!\" save-all stop\n");
#ifdef _WIN32
puts("Press enter to exit.");
getchar();
puts("Press enter to exit.");
getchar();
#endif
exit(EXIT_SUCCESS);
@ -301,10 +322,10 @@ void net_init_WSA(void)
void net_close(int sd)
{
#ifdef _WIN32
closesocket(sd);
WSACleanup();
closesocket(sd);
WSACleanup();
#else
close(sd);
close(sd);
#endif
}
@ -323,16 +344,16 @@ int net_connect(const char *host, const char *port)
hints.ai_protocol = IPPROTO_TCP;
#ifdef _WIN32
net_init_WSA();
net_init_WSA();
#endif
int ret = getaddrinfo(host, port, &hints, &server_info);
if (ret != 0) {
log_error("Name resolution failed.\n");
#ifdef _WIN32
log_error("Error %d: %s", ret, gai_strerror(ret));
log_error("Error %d: %s", ret, gai_strerror(ret));
#else
log_error("Error %d: %s\n", ret, gai_strerror(ret));
log_error("Error %d: %s\n", ret, gai_strerror(ret));
#endif
exit(EXIT_FAILURE);
@ -358,7 +379,7 @@ int net_connect(const char *host, const char *port)
/* TODO (Tiiffi): Check why windows does not report errors */
log_error("Connection failed.\n");
#ifndef _WIN32
log_error("Error %d: %s\n", errno, strerror(errno));
log_error("Error %d: %s\n", errno, strerror(errno));
#endif
freeaddrinfo(server_info);
@ -389,6 +410,9 @@ bool net_send_packet(int sd, rc_packet *packet)
return true;
}
// TODO: Fix the wonky behaviour when server quit/exit/stop/close
// command is issued. Client should close gracefully without errors.
rc_packet *net_recv_packet(int sd)
{
int32_t psize;
@ -470,9 +494,9 @@ void print_color(int color)
else return;
#ifndef _WIN32
fputs(colors[color], stdout);
fputs(colors[color], stdout);
#else
SetConsoleTextAttribute(console_handle, color);
SetConsoleTextAttribute(console_handle, color);
#endif
}
}
@ -481,52 +505,67 @@ void print_color(int color)
void packet_print(rc_packet *packet)
{
uint8_t *data = packet->data;
int i;
if (flag_raw_output == 1) {
for (int i = 0; data[i] != 0; ++i) {
putchar(data[i]);
}
fputs((char *) data, stdout);
return;
}
int i;
// Newline fix for Minecraft
if (global_valve_protocol == false) {
const char test[] = "Unknown or incomplete command, see below for error";
size_t test_size = sizeof test - 1;
if (strncmp((char *) data, test, test_size) == 0) {
fwrite(data, test_size, 1, stdout);
putchar('\n');
data = &data[test_size];
}
}
int default_color = 0;
#ifdef _WIN32
CONSOLE_SCREEN_BUFFER_INFO console_info;
if (GetConsoleScreenBufferInfo(console_handle, &console_info) != 0) {
default_color = console_info.wAttributes + 0x30;
} else default_color = 0x37;
CONSOLE_SCREEN_BUFFER_INFO console_info;
if (GetConsoleScreenBufferInfo(console_handle, &console_info) != 0) {
default_color = console_info.wAttributes + 0x30;
}
else default_color = 0x37;
#endif
// colors enabled so try to handle the bukkit colors for terminal
if (flag_disable_colors == 0) {
for (i = 0; data[i] != 0; ++i) {
if (data[i] == 0x0A) print_color(default_color);
else if(data[i] == 0xc2 && data[i + 1] == 0xa7) {
i += 2;
bool slash = false;
bool colors_detected = false;
for (i = 0; data[i] != 0; ++i)
{
if (data[i] == 0x0A) {
print_color(default_color);
}
else if(data[i] == 0xc2 && data[i + 1] == 0xa7) {
// Disable new line fixes if Bukkit colors are detected
colors_detected = true;
i += 2;
if (flag_disable_colors == 0) {
print_color(data[i]);
continue;
}
putchar(data[i]);
continue;
}
print_color(default_color); // cancel coloring
}
// strip colors
else {
for (i = 0; data[i] != 0; ++i) {
if (data[i] == 0xc2 && data[i + 1] == 0xa7) {
i += 2;
continue;
}
putchar(data[i]);
// Add missing newlines
if (colors_detected == false && global_minecraft_newline_fix && data[i] == '/') {
slash ? putchar('\n') : (slash = true);
}
putchar(data[i]);
}
print_color(default_color); // cancel coloring
// print newline if string has no newline
if (data[i - 1] != 10 && data[i - 1] != 13) {
if (data[i - 1] != '\n') {
putchar('\n');
}
fflush(stdout);
}
rc_packet *packet_build(int id, int cmd, char s[static 1])
@ -564,7 +603,7 @@ bool rcon_auth(int sock, char *passwd)
return 0; // send failed
}
receive:
receive:
packet = net_recv_packet(sock);
if (packet == NULL)
return 0;
@ -573,6 +612,7 @@ receive:
* so we have to check packet type and try again if necessary.
*/
if (packet->cmd != RCON_AUTH_RESPONSE) {
global_valve_protocol = true;
goto receive;
}
@ -580,9 +620,11 @@ receive:
return packet->id == -1 ? false : true;
}
// TODO: Add proper error handling and reporting!
int rcon_command(int sock, char *command)
{
if (global_valve_protocol == false && strcasecmp(command, "help") == 0)
global_minecraft_newline_fix = true;
rc_packet *packet = packet_build(RCON_PID, RCON_EXEC_COMMAND, command);
if (packet == NULL) {
log_error("Error: packet build() failed!\n");
@ -594,21 +636,60 @@ int rcon_command(int sock, char *command)
return 0;
}
packet = net_recv_packet(sock);
// Workaround to handle valve multipacket responses
// This one does not require using select()
packet = packet_build(0xBADA55, 0xBADA55, "");
if (packet == NULL) {
log_error("Error: net_recv_packet() failed!\n");
log_error("Error: packet build() failed!\n");
return 0;
}
if (packet->id != RCON_PID) {
log_error("Error: invalid packet id!\n");
if (!net_send_packet(sock, packet)) {
log_error("Error: net_send_packet() failed!\n");
return 0;
}
if (!flag_silent_mode) {
if (packet->size > 10)
packet_print(packet);
// initialize stuff for select()
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sock, &read_fds);
// Set 5 second timeout
struct timeval timeout = {0};
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int incoming = 0;
do {
packet = net_recv_packet(sock);
if (packet == NULL) {
log_error("Error: net_recv_packet() failed!\n");
return 0;
}
// Check for packet id and multipacket guard id
if (packet->id != RCON_PID && packet->id != 0xBADA55) {
log_error("Error: invalid packet id!\n");
return 0;
}
if (packet->id == 0xBADA55) break;
if (flag_silent_mode == false) {
if (packet->size > 10)
packet_print(packet);
}
int result = select(sock + 1, &read_fds, NULL, NULL, &timeout);
if (result == -1) {
log_error("Error: select() failed!\n");
return 0;
}
incoming = (result > 0 && FD_ISSET(sock, &read_fds));
}
while(incoming);
return 1;
}
@ -628,9 +709,9 @@ int run_commands(int argc, char *argv[])
if (flag_wait_seconds > 0) {
#ifdef _WIN32
Sleep(flag_wait_seconds * 1000);
Sleep(flag_wait_seconds * 1000);
#else
sleep(flag_wait_seconds);
sleep(flag_wait_seconds);
#endif
}
}
@ -641,15 +722,14 @@ int run_terminal_mode(int sock)
{
char command[MAX_COMMAND_LENGTH] = {0};
puts("Logged in.\nType 'Q' or press Ctrl-D / Ctrl-C to disconnect.");
puts("Logged in. Press Ctrl-D or Ctrl-C to disconnect.");
while (global_connection_alive) {
putchar('>');
fflush(stdout);
int len = get_line(command, MAX_COMMAND_LENGTH);
if (len < 1) continue;
if (strcasecmp(command, "Q") == 0) break;
if (len < 1) continue;
if (len > 0 && global_connection_alive) {
if (!rcon_command(sock, command)) {
@ -659,23 +739,81 @@ int run_terminal_mode(int sock)
/* Special case for "stop" command to prevent server-side bug.
* https://bugs.mojang.com/browse/MC-154617
*
* NOTE: This is hacky workaround which should be handled better to
* ensure compatibility with other servers using source RCON.
* NOTE: strcasecmp() is POSIX function.
*
* NOTE: Not sure if this still a problem?!
*/
if (strcasecmp(command, "stop") == 0) {
break;
if (global_valve_protocol == false && strcasecmp(command, "stop") == 0) {
// Timeout to before checking if connection was closed
#ifdef _WIN32
Sleep(2 * 1000);
#else
sleep(2);
#endif
char tmp[1];
#ifdef _WIN32
// TODO: More Windows side testing!
int result = recv(sock, tmp, sizeof(tmp), MSG_PEEK | 0);
#else
int result = recv(sock, tmp, sizeof(tmp), MSG_PEEK | MSG_DONTWAIT);
#endif
if (result == 0) {
break; // Connection closed
}
// TODO: Check for return values and errors!
}
}
return EXIT_SUCCESS;
}
#ifdef _WIN32
char *utf8_getline(char *buf, int size, FILE *stream)
{
// Widechar fgets
wchar_t in[MAX_COMMAND_LENGTH] = {0};
wchar_t *result = fgetws(in, MAX_COMMAND_LENGTH, stream);
if (result == NULL) {
return NULL;
}
// Calculates UTF-8 buffer size
int required_size = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL);
if (size < required_size) {
// TODO: Proper error handling & reporting
return NULL;
}
if (WideCharToMultiByte(CP_UTF8, 0, in, -1, buf, size, NULL, NULL) == 0) {
// TODO: Proper error handling & reporting
return NULL;
}
return buf;
}
#endif
void flush_input(void)
{
#ifdef _WIN32
FlushConsoleInputBuffer(console_handle);
#else
tcflush(STDIN_FILENO, TCIFLUSH);
#endif
}
// gets line from stdin and deals with rubbish left in the input buffer
int get_line(char *buffer, int bsize)
{
#ifdef _WIN32
char *ret = utf8_getline(buffer, bsize, stdin);
#else
char *ret = fgets(buffer, bsize, stdin);
#endif
flush_input();
if (ret == NULL) {
if (ferror(stdin)) {
log_error("Error %d: %s\n", errno, strerror(errno));
@ -687,14 +825,17 @@ int get_line(char *buffer, int bsize)
// remove unwanted characters from the buffer
buffer[strcspn(buffer, "\r\n")] = '\0';
int len = strlen(buffer);
// clean input buffer if needed
#if 0
// clean input buffer if needed
#ifndef _WIN32
if (len == bsize - 1) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
#endif
#endif
return len;
}