diff --git a/Makefile b/Makefile index 2aa2cbd..b2410cf 100644 --- a/Makefile +++ b/Makefile @@ -7,14 +7,16 @@ CFLAGS_DEBUG = -O0 -ggdb -DDEBUG -fsanitize=address -static-libasan TEST_LIBS = -lcmocka TEST_INCLUDES = $(shell pkg-config --cflags cmocka) -CFLAGS_TEST = -O0 -ggdb -DTEST -fsanitize=address -static-libasan -fprofile-instr-generate -fcoverage-mapping $(TEST_LIBS) $(TEST_INCLUDES) +CFLAGS_TEST = -O0 -ggdb -DTEST -fsanitize=address -static-libasan -fprofile-instr-generate -fcoverage-mapping $(TEST_INCLUDES) +LDFLAGS_TEST = $(TEST_LIBS) prefix = /usr/local exec_prefix = $(prefix) bindir = $(exec_prefix)/bin SOURCE_FOLDER = src -SOURCE = main.c +SOURCES = $(wildcard $(SOURCE_FOLDER)/*.c) +APP_SOURCES = $(filter-out $(SOURCE_FOLDER)/main.c,$(SOURCES)) TARGET = wamf TARGET_DEBUG = wamf-debug OUTPUT_FOLDER = output @@ -22,7 +24,7 @@ OUTPUT = $(OUTPUT_FOLDER)/$(TARGET) OUTPUT_DEBUG = $(OUTPUT_FOLDER)/$(TARGET_DEBUG) TEST_FOLDER = test -TEST_SOURCE = test.c +TEST_SOURCES = $(wildcard $(TEST_FOLDER)/*.c) TEST_OUTPUT_FOLDER = $(OUTPUT_FOLDER)/test TEST_OUTPUT = $(TEST_OUTPUT_FOLDER)/test @@ -36,19 +38,19 @@ REMOTE_PATH = $(REMOTE_USER)@$(REMOTE_HOST):$(REMOTE_DIR) all: $(OUTPUT) $(OUTPUT_FOLDER): - mkdir $(OUTPUT_FOLDER) + mkdir -p $(OUTPUT_FOLDER) $(TEST_OUTPUT_FOLDER): $(OUTPUT_FOLDER) - mkdir $(TEST_OUTPUT_FOLDER) + mkdir -p $(TEST_OUTPUT_FOLDER) -$(OUTPUT) release: $(SOURCE_FOLDER)/$(SOURCE) $(OUTPUT_FOLDER) - $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_RELEASE) -o $(OUTPUT) $(SOURCE_FOLDER)/$(SOURCE) +$(OUTPUT) release: $(SOURCES) $(OUTPUT_FOLDER) + $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_RELEASE) -o $(OUTPUT) $(SOURCES) -$(OUTPUT_DEBUG) debug: $(SOURCE_FOLDER)/$(SOURCE) $(OUTPUT_FOLDER) - $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_DEBUG) -o $(OUTPUT_DEBUG) $(SOURCE_FOLDER)/$(SOURCE) +$(OUTPUT_DEBUG) debug: $(SOURCES) $(OUTPUT_FOLDER) + $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_DEBUG) -o $(OUTPUT_DEBUG) $(SOURCES) -$(TEST_OUTPUT): $(TEST_FOLDER)/$(TEST_SOURCE) $(TEST_OUTPUT_FOLDER) - $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_TEST) -o $(TEST_OUTPUT) $(TEST_FOLDER)/$(TEST_SOURCE) +$(TEST_OUTPUT): $(APP_SOURCES) $(TEST_SOURCES) $(TEST_OUTPUT_FOLDER) + $(CC) $(CFLAGS) $(CFLAGS_WARNINGS) $(CFLAGS_TEST) -o $(TEST_OUTPUT) $(APP_SOURCES) $(TEST_SOURCES) $(LDFLAGS_TEST) install: $(OUTPUT) install -Dm755 $(OUTPUT) -t $(bindir) @@ -60,7 +62,7 @@ deploy: @echo "Creating remote directory..." ssh $(REMOTE_USER)@$(REMOTE_HOST) "mkdir -p $(REMOTE_DIR)" @echo "Copying source files to $(REMOTE_HOST)..." - scp $(SOURCE_FOLDER)/$(SOURCE) $(REMOTE_PATH)/$(SOURCE_FOLDER) + scp $(SOURCE_FOLDER)/* $(REMOTE_PATH)/$(SOURCE_FOLDER) scp Makefile $(REMOTE_PATH) @echo "Files deployed successfully" diff --git a/src/main.c b/src/main.c index 394c1ef..7a1557a 100644 --- a/src/main.c +++ b/src/main.c @@ -1,39 +1,11 @@ -#include #include #include -#include #include -#include -#include -#include -#include -#include -#include + +#include "rcon.h" const char *LOCALHOST = "127.0.0.1"; -#define SERVERDATA_AUTH 3 -#define SERVERDATA_AUTH_RESPONSE 2 -#define SERVERDATA_EXECCOMMAND 2 -#define SERVERDATA_RESPONSE_VALUE 0 - -typedef struct { - // size of the remainder of the packet - int32_t size; - // packet id (client sets, server echoes, -1 on auth failure) - int32_t id; - // packet type (see the SERVERDATA_* constants) - int32_t type; - // null-terminated command string (for auth and exec) or response string (from server) - char body[4096]; - // yet another null byte after the body (required by protocol) - uint8_t _padding; -} rcon_packet_t; - -// Minimum and maximum values for the `size` field of a valid packet. -#define MIN_SIZE (sizeof(int32_t) * 2 + 2) -#define MAX_SIZE (sizeof(rcon_packet_t) - sizeof(int32_t)) - int read_env_int(const char *var) { const char* port_str = getenv(var); if (port_str == NULL) { @@ -49,188 +21,6 @@ int read_env_int(const char *var) { return (int)val; } -int rcon_connect(int port) { - int sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) { - perror("socket creation failed"); - exit(EXIT_FAILURE); - } - - struct timeval tv; - tv.tv_sec = 2; - tv.tv_usec = 0; - setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - - struct sockaddr_in server_addr; - memset(&server_addr, 0, sizeof(server_addr)); - server_addr.sin_family = AF_INET; - server_addr.sin_port = htons(port); - - if (inet_pton(AF_INET, LOCALHOST, &server_addr.sin_addr) <= 0) { - perror("invalid address"); - close(sockfd); - exit(EXIT_FAILURE); - } - - if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { - perror("connection failed"); - close(sockfd); - exit(EXIT_FAILURE); - } - - printf("connected to rcon server\n"); - return sockfd; -} - -void send_packet(int sockfd, int32_t id, int32_t type, const char *body) { - size_t body_len = strlen(body); - - size_t packet_size = sizeof(int32_t) * 3 + body_len + 2; - unsigned char *packet = malloc(packet_size); - if (!packet) { - perror("malloc failed"); - return; - } - - int32_t size = htole32(packet_size - sizeof(int32_t)); - memcpy(packet, &size, sizeof(int32_t)); - - int32_t id_ = htole32(id); - memcpy(packet + 4, &id_, sizeof(int32_t)); - - int32_t type_ = htole32(type); - memcpy(packet + 8, &type_, sizeof(int32_t)); - - memcpy(packet + 12, body, body_len); - packet[3 * sizeof(int32_t) + body_len] = 0; - packet[3 * sizeof(int32_t) + body_len + 1] = 0; - - ssize_t sent = send(sockfd, packet, packet_size, 0); - if (sent < 0) { - perror("send failed"); - } - - free(packet); -} - -int recv_packet(int sockfd, rcon_packet_t *packet) { - ssize_t bytes; - - bytes = recv(sockfd, &packet->size, sizeof(int32_t), MSG_WAITALL); - if (bytes <= 0) { - if (bytes == 0) { - fprintf(stderr, "connection closed by server\n"); - } else if (errno == EAGAIN || errno == EWOULDBLOCK) { - return -2; // Timeout - } else { - perror("recv size failed"); - } - return -1; - } - packet->size = le32toh(packet->size); - - if (packet->size < (int32_t)MIN_SIZE || packet->size > (int32_t)MAX_SIZE) { - fprintf(stderr, "invalid packet size: %d\n", packet->size); - return -1; - } - - bytes = recv(sockfd, &packet->id, sizeof(int32_t), MSG_WAITALL); - if (bytes <= 0) { - perror("recv id failed"); - return -1; - } - packet->id = le32toh(packet->id); - - bytes = recv(sockfd, &packet->type, sizeof(int32_t), MSG_WAITALL); - if (bytes <= 0) { - perror("recv type failed"); - return -1; - } - packet->type = le32toh(packet->type); - - int body_size = packet->size - sizeof(int32_t) * 2 - 2; - if (body_size > 0 && body_size < (int)sizeof(packet->body)) { - bytes = recv(sockfd, packet->body, body_size, MSG_WAITALL); - if (bytes <= 0) { - perror("recv body failed"); - return -1; - } - packet->body[body_size] = '\0'; - - bytes = recv(sockfd, &packet->_padding, 1, MSG_WAITALL); - if (bytes <= 0) { - perror("recv padding failed"); - return -1; - } - } else { - packet->size = sizeof(int32_t) * 2 + 2; - packet->body[0] = '\0'; - } - - return 0; -} - -int rcon_authenticate(int sockfd, const char *password) { - printf("authenticating with password: '%s'\n", password); - - send_packet(sockfd, 1, SERVERDATA_AUTH, password); - - printf("waiting for auth response...\n"); - - rcon_packet_t response; - if (recv_packet(sockfd, &response) < 0) { - fprintf(stderr, "failed to receive auth response\n"); - return -1; - } - - printf("received auth packet: id=%d, type=%d\n", response.id, response.type); - - // Check if authentication failed (id == -1) - if (response.id == -1) { - fprintf(stderr, "authentication failed - invalid password\n"); - return -1; - } - - // Try to read second packet (may timeout, which is OK) - rcon_packet_t response2; - int result = recv_packet(sockfd, &response2); - if (result == 0) { - printf("received second auth packet: id=%d, type=%d\n", response2.id, response2.type); - } else if (result == -2) { - printf("no second packet (timeout - this is normal)\n"); - } - - printf("authenticated successfully\n"); - return 0; -} - -char* rcon_command_multipacket(int sockfd, const char *command) { - static char result[65536]; - result[0] = '\0'; - - int cmd_id = 100; - int dummy_id = 101; - - send_packet(sockfd, cmd_id, SERVERDATA_EXECCOMMAND, command); - send_packet(sockfd, dummy_id, SERVERDATA_EXECCOMMAND, ""); - - while (1) { - rcon_packet_t response; - if (recv_packet(sockfd, &response) < 0) { - fprintf(stderr, "failed to receive command response\n"); - return NULL; - } - if (response.id == dummy_id) { - break; - } - if (response.type == SERVERDATA_RESPONSE_VALUE && response.id == cmd_id) { - strncat(result, response.body, sizeof(result) - strlen(result) - 1); - } - } - - return result; -} - int main(void) { const int PORT_NUM = read_env_int("RCON_PORT_NUMBER"); const char *password = getenv("RCON_PASSWORD"); @@ -245,7 +35,7 @@ int main(void) { printf("password: '%s'\n", password); printf("==========================\n\n"); - int sockfd = rcon_connect(PORT_NUM); + int sockfd = rcon_connect(LOCALHOST, PORT_NUM); if (rcon_authenticate(sockfd, password) < 0) { close(sockfd); diff --git a/src/rcon.c b/src/rcon.c new file mode 100644 index 0000000..05e86d2 --- /dev/null +++ b/src/rcon.c @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include +#include + +#include "rcon.h" + +int rcon_connect(const char* address, int port) { + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) { + perror("socket creation failed"); + exit(EXIT_FAILURE); + } + + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(port); + + if (inet_pton(AF_INET, address, &server_addr.sin_addr) <= 0) { + perror("invalid address"); + close(sockfd); + exit(EXIT_FAILURE); + } + + if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + perror("connection failed"); + close(sockfd); + exit(EXIT_FAILURE); + } + + printf("connected to rcon server\n"); + return sockfd; +} + +void send_packet(int sockfd, int32_t id, int32_t type, const char *body) { + size_t body_len = strlen(body); + + size_t packet_size = sizeof(int32_t) * 3 + body_len + 2; + unsigned char *packet = malloc(packet_size); + if (!packet) { + perror("malloc failed"); + return; + } + + int32_t size = htole32(packet_size - sizeof(int32_t)); + memcpy(packet, &size, sizeof(int32_t)); + + int32_t id_ = htole32(id); + memcpy(packet + 4, &id_, sizeof(int32_t)); + + int32_t type_ = htole32(type); + memcpy(packet + 8, &type_, sizeof(int32_t)); + + memcpy(packet + 12, body, body_len); + packet[3 * sizeof(int32_t) + body_len] = 0; + packet[3 * sizeof(int32_t) + body_len + 1] = 0; + + ssize_t sent = send(sockfd, packet, packet_size, 0); + if (sent < 0) { + perror("send failed"); + } + + free(packet); +} + +int recv_packet(int sockfd, rcon_packet_t *packet) { + ssize_t bytes; + + bytes = recv(sockfd, &packet->size, sizeof(int32_t), MSG_WAITALL); + if (bytes <= 0) { + if (bytes == 0) { + fprintf(stderr, "connection closed by server\n"); + } else if (errno == EAGAIN || errno == EWOULDBLOCK) { + return -2; // Timeout + } else { + perror("recv size failed"); + } + return -1; + } + packet->size = le32toh(packet->size); + + if (packet->size < (int32_t)RCON_PACKET_MIN_SIZE || packet->size > (int32_t)RCON_PACKET_MAX_SIZE) { + fprintf(stderr, "invalid packet size: %d\n", packet->size); + return -1; + } + + bytes = recv(sockfd, &packet->id, sizeof(int32_t), MSG_WAITALL); + if (bytes <= 0) { + perror("recv id failed"); + return -1; + } + packet->id = le32toh(packet->id); + + bytes = recv(sockfd, &packet->type, sizeof(int32_t), MSG_WAITALL); + if (bytes <= 0) { + perror("recv type failed"); + return -1; + } + packet->type = le32toh(packet->type); + + int body_size = packet->size - sizeof(int32_t) * 2 - 2; + if (body_size > 0 && body_size < (int)sizeof(packet->body)) { + bytes = recv(sockfd, packet->body, body_size, MSG_WAITALL); + if (bytes <= 0) { + perror("recv body failed"); + return -1; + } + packet->body[body_size] = '\0'; + + bytes = recv(sockfd, &packet->_padding, 1, MSG_WAITALL); + if (bytes <= 0) { + perror("recv padding failed"); + return -1; + } + } else { + packet->size = sizeof(int32_t) * 2 + 2; + packet->body[0] = '\0'; + } + + return 0; +} + +int rcon_authenticate(int sockfd, const char *password) { + printf("authenticating with password: '%s'\n", password); + + send_packet(sockfd, 1, RCON_SERVERDATA_AUTH, password); + + printf("waiting for auth response...\n"); + + rcon_packet_t response; + if (recv_packet(sockfd, &response) < 0) { + fprintf(stderr, "failed to receive auth response\n"); + return -1; + } + + printf("received auth packet: id=%d, type=%d\n", response.id, response.type); + + // Check if authentication failed (id == -1) + if (response.id == -1) { + fprintf(stderr, "authentication failed - invalid password\n"); + return -1; + } + + // Try to read second packet (may timeout, which is OK) + rcon_packet_t response2; + int result = recv_packet(sockfd, &response2); + if (result == 0) { + printf("received second auth packet: id=%d, type=%d\n", response2.id, response2.type); + } else if (result == -2) { + printf("no second packet (timeout - this is normal)\n"); + } + + printf("authenticated successfully\n"); + return 0; +} + +char* rcon_command_multipacket(int sockfd, const char *command) { + static char result[65536]; + result[0] = '\0'; + + int cmd_id = 100; + int dummy_id = 101; + + send_packet(sockfd, cmd_id, RCON_SERVERDATA_EXECCOMMAND, command); + send_packet(sockfd, dummy_id, RCON_SERVERDATA_EXECCOMMAND, ""); + + while (1) { + rcon_packet_t response; + if (recv_packet(sockfd, &response) < 0) { + fprintf(stderr, "failed to receive command response\n"); + return NULL; + } + if (response.id == dummy_id) { + break; + } + if (response.type == RCON_SERVERDATA_RESPONSE_VALUE && response.id == cmd_id) { + strncat(result, response.body, sizeof(result) - strlen(result) - 1); + } + } + + return result; +} diff --git a/src/rcon.h b/src/rcon.h new file mode 100644 index 0000000..3559f2b --- /dev/null +++ b/src/rcon.h @@ -0,0 +1,33 @@ +#include + +#define RCON_SERVERDATA_AUTH 3 +#define RCON_SERVERDATA_AUTH_RESPONSE 2 +#define RCON_SERVERDATA_EXECCOMMAND 2 +#define RCON_SERVERDATA_RESPONSE_VALUE 0 + +typedef struct { + // size of the remainder of the packet + int32_t size; + // packet id (client sets, server echoes, -1 on auth failure) + int32_t id; + // packet type (see the SERVERDATA_* constants) + int32_t type; + // null-terminated command string (for auth and exec) or response string (from server) + char body[4096]; + // yet another null byte after the body (required by protocol) + uint8_t _padding; +} rcon_packet_t; + +// Minimum and maximum values for the `size` field of a valid packet. +#define RCON_PACKET_MIN_SIZE (sizeof(int32_t) * 2 + 2) +#define RCON_PACKET_MAX_SIZE (sizeof(rcon_packet_t) - sizeof(int32_t)) + +int rcon_connect(const char* address, int port); + +void send_packet(int sockfd, int32_t id, int32_t type, const char *body); + +int recv_packet(int sockfd, rcon_packet_t *packet); + +int rcon_authenticate(int sockfd, const char *password); + +char* rcon_command_multipacket(int sockfd, const char *command); diff --git a/test/test.c b/test/test_rcon.c similarity index 85% rename from test/test.c rename to test/test_rcon.c index b815a66..1b985c1 100644 --- a/test/test.c +++ b/test/test_rcon.c @@ -1,19 +1,9 @@ -#define _GNU_SOURCE - -#include -#include -#include -#include #include #include -#include #include - #include -#define main wamf_main -#include "../src/main.c" -#undef main +#include "../src/rcon.h" static void assert_packet_equals(const rcon_packet_t *packet, int32_t expected_id, int32_t expected_type, @@ -33,7 +23,7 @@ static void test_serialization_roundtrip_command_packet(void **state) { assert_int_equal(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); const int32_t id = 123; - const int32_t type = SERVERDATA_EXECCOMMAND; + const int32_t type = RCON_SERVERDATA_EXECCOMMAND; const char *body = "execute as @a run data get entity @s Pos"; send_packet(sv[0], id, type, body); @@ -53,7 +43,7 @@ static void test_serialization_roundtrip_auth_packet(void **state) { assert_int_equal(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); const int32_t id = 1; - const int32_t type = SERVERDATA_AUTH; + const int32_t type = RCON_SERVERDATA_AUTH; const char *body = "correct horse battery staple"; send_packet(sv[0], id, type, body); @@ -73,7 +63,7 @@ static void test_serialization_roundtrip_empty_body_packet(void **state) { assert_int_equal(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); const int32_t id = 101; - const int32_t type = SERVERDATA_EXECCOMMAND; + const int32_t type = RCON_SERVERDATA_EXECCOMMAND; const char *body = ""; send_packet(sv[0], id, type, body); @@ -96,11 +86,11 @@ static void test_serialization_roundtrip_max_supported_body(void **state) { memset(body, 'A', sizeof(body) - 1); body[sizeof(body) - 1] = '\0'; - send_packet(sv[0], 777, SERVERDATA_RESPONSE_VALUE, body); + send_packet(sv[0], 777, RCON_SERVERDATA_RESPONSE_VALUE, body); rcon_packet_t packet = {0}; assert_int_equal(recv_packet(sv[1], &packet), 0); - assert_packet_equals(&packet, 777, SERVERDATA_RESPONSE_VALUE, body); + assert_packet_equals(&packet, 777, RCON_SERVERDATA_RESPONSE_VALUE, body); close(sv[0]); close(sv[1]);