mirror of
				https://github.com/Tiiffi/mcrcon.git
				synced 2025-10-31 13:21:07 -04:00 
			
		
		
		
	Compare commits
	
		
			14 Commits
		
	
	
		
			v0.7.3-dev
			...
			4a1da8d373
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4a1da8d373 | |||
| b1b46ca08c | |||
| 2bb1fafdaa | |||
| 0fb17971c0 | |||
| ec11d77e89 | |||
| 2d29741691 | |||
| cc77044df1 | |||
| dba07aacf7 | |||
| bd76b897de | |||
| 1106f27700 | |||
| 00fc3b5bcb | |||
| a0fe9e1645 | |||
| 5f460e8912 | |||
| 6fed74ba74 | 
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -1,8 +1,12 @@ | ||||
| #### 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() | ||||
| @ -11,7 +15,7 @@ | ||||
| ###### 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 | ||||
|  | ||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										2
									
								
								mcrcon.1
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								mcrcon.1
									
									
									
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										231
									
								
								mcrcon.c
									
									
									
									
									
								
							
							
						
						
									
										231
									
								
								mcrcon.c
									
									
									
									
									
								
							| @ -31,17 +31,20 @@ | ||||
| #include <errno.h> | ||||
| #include <unistd.h> | ||||
|  | ||||
|  | ||||
| #ifdef _WIN32 | ||||
|     #include <winsock2.h> | ||||
|     #include <windows.h> | ||||
|     #include <ws2tcpip.h> | ||||
|     #include <fcntl.h> | ||||
|     #include <wchar.h> | ||||
| #else | ||||
|     #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,6 +59,8 @@ | ||||
| #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 | ||||
| @ -95,7 +100,6 @@ void        packet_print(rc_packet *packet); | ||||
| bool        rcon_auth(int sock, char *passwd); | ||||
| int         rcon_command(int sock, char *command); | ||||
|  | ||||
|  | ||||
| // ============================================= | ||||
| //  GLOBAL VARIABLES | ||||
| // ============================================= | ||||
| @ -104,11 +108,17 @@ 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 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 | ||||
| } | ||||
|  | ||||
| #define MAX_WAIT_TIME 600 | ||||
|  | ||||
| unsigned int mcrcon_parse_seconds(char *str) | ||||
| { | ||||
|     char *end; | ||||
| @ -166,10 +181,6 @@ int main(int argc, char *argv[]) | ||||
|     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 | ||||
| @ -223,6 +234,16 @@ int main(int argc, char *argv[]) | ||||
|     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 | ||||
| @ -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; | ||||
| @ -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; | ||||
|     } | ||||
|     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); | ||||
|     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; | ||||
|         } | ||||
|  | ||||
|         // 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 | ||||
| 	} | ||||
| 	// strip colors | ||||
| 	else { | ||||
| 		for (i = 0; data[i] != 0; ++i) { | ||||
| 			if (data[i] == 0xc2 && data[i + 1] == 0xa7) { | ||||
| 				i += 2; | ||||
| 				continue; | ||||
| 			}	 | ||||
| 			putchar(data[i]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|     // 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]) | ||||
| @ -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,22 +636,61 @@ int rcon_command(int sock, char *command) | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     // Workaround to handle valve multipacket responses | ||||
|     // This one does not require using select() | ||||
|     packet = packet_build(0xBADA55, 0xBADA55, ""); | ||||
|     if (packet == NULL) { | ||||
|         log_error("Error: packet build() failed!\n"); | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     if (!net_send_packet(sock, packet)) { | ||||
|         log_error("Error: net_send_packet() failed!\n"); | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     // 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; | ||||
|         } | ||||
|  | ||||
| 	if (packet->id != RCON_PID) { | ||||
|         // 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 (!flag_silent_mode) { | ||||
|         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; | ||||
| } | ||||
|  | ||||
| @ -641,16 +722,15 @@ 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 > 0 && global_connection_alive) { | ||||
|             if (!rcon_command(sock, command)) { | ||||
|                 return EXIT_FAILURE; | ||||
| @ -660,22 +740,80 @@ 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,7 @@ 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 (len == bsize - 1) { | ||||
| 		int ch; | ||||
| 		while ((ch = getchar()) != '\n' && ch != EOF); | ||||
| 	} | ||||
|  | ||||
|     return len; | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	