From a14f053132ca68768f891e1c2394db50fc019e0f Mon Sep 17 00:00:00 2001 From: Ajurna Date: Wed, 30 Jul 2025 07:36:07 +0100 Subject: [PATCH] server03 started --- CMakeLists.txt | 4 +- server03.c | 175 ++++++++++++++++++++++++++++++++++ server03.h | 42 ++++++++ server03_chat_server_tests.py | 135 ++++++++++++++++++++++++++ 4 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 server03.c create mode 100644 server03.h create mode 100644 server03_chat_server_tests.py diff --git a/CMakeLists.txt b/CMakeLists.txt index b554a4b..bca5f85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,4 +25,6 @@ target_link_libraries(server01 PRIVATE wsock32 ws2_32 json-c::json-c) add_executable(server02 server02.c server02.h ${COMMON_SOURCES}) -target_link_libraries(server02 wsock32 ws2_32) \ No newline at end of file +target_link_libraries(server02 wsock32 ws2_32) +add_executable(server03 server03.c server03.h ${COMMON_SOURCES}) +target_link_libraries(server03 wsock32 ws2_32) \ No newline at end of file diff --git a/server03.c b/server03.c new file mode 100644 index 0000000..17a0f0c --- /dev/null +++ b/server03.c @@ -0,0 +1,175 @@ +// +// Created by Ajurna on 29/07/2025. +// + +#include "server03.h" + +#include + +#include "data.h" +#include +#include +#include + +pthread_mutex_t mutex; +pthread_cond_t cond; +#define WELCOME "Welcome to budgetchat! What shall I call you?\n" + + + +int main() { + SOCKET server = get_listen_socket(); + SOCKADDR_IN clientAddr; + SOCKET client; + connections_t *connections = connections_create(64); + int clientAddrSize = sizeof(clientAddr); + int connection_number = 1; + printf("Listening for incoming connections...\n"); + while((client = accept(server, (SOCKADDR *)&clientAddr, &clientAddrSize)) != INVALID_SOCKET) + { + handle_args_t *args = malloc(sizeof(handle_args_t)); + args->client = client; + args->connection = connection_number++; + args->connections = *connections; + pthread_t thread; + pthread_create(&thread, nullptr, handle_connection, args); + } + connections_destroy(connections); + return 0; +} + +void *handle_connection(void *args) { + handle_args_t *handleArgs = args; + char buffer[1024] = {0}; + int bytesReceived; + char *message[1024]; + char_array_t *data = char_array_create(1024); + send(handleArgs->client, WELCOME, sizeof(WELCOME), 0); + bytesReceived = recv(handleArgs->client, buffer, sizeof(buffer), 0); + char_array_append(data, buffer, bytesReceived); + char *username = char_array_get_until_char(data, '\n'); + if (username == NULL) { + printf("{%d} Failed to parse username\n", handleArgs->connection); + connections_remove(&handleArgs->connections, handleArgs->client); + free(handleArgs); + exit(3); + } + username = trim(username); + if (!username_is_valid(username)) { + printf("{%d} Invalid username\n", handleArgs->connection); + connections_remove(&handleArgs->connections, handleArgs->client); + free(handleArgs); + exit(3); + } + pthread_mutex_lock(&mutex); + strcat(message, "* The room contains: "); + for (int i = 0; i < handleArgs->connections.size; i++) { + strcat(message, handleArgs->connections.clients[i].username); + strcat(message, " "); + } + message[strlen(message)-1] = '\n'; + send(handleArgs->client, message, strlen(message), 0); + pthread_mutex_unlock(&mutex); + + connections_append(&handleArgs->connections, handleArgs->client, username); + sprintf(message, "* %s has entered the room\n", username); + broadcast(&handleArgs->connections, &handleArgs->client, message); + + printf("{%d} Username: %s\n", handleArgs->connection, username); + + while ((bytesReceived = recv(handleArgs->client, buffer, sizeof(buffer), 0)) > 0) { + printf("{%d} Client sent: |%d| \n", handleArgs->connection, bytesReceived); + char_array_append(data, buffer, bytesReceived); + char *request; + while ((request = char_array_get_until_char(data, '\n')) != NULL) { + sprintf(message, "[%s] %s\n", username, request); + } + memset(buffer, 0, sizeof(buffer)); + } + + free(args); + return NULL; +} + +connections_t *connections_create(const int size) { + connections_t *connections = malloc(sizeof(connections_t)); + connections->size = size; + connections->clients = calloc(size, sizeof(client_connection_t)); + return connections; +} +void connections_destroy(connections_t *connections) { + free(connections->clients); + free(connections); +} +void connections_append(connections_t *connections, SOCKET client, char *username) { + if (connections == NULL) { + exit(1); + } + pthread_mutex_lock(&mutex); + size_t new_size = connections->size + 1; + if (new_size > connections->size) { + connections->size = connections->size+64; + client_connection_t *new_array = realloc(connections->clients, connections->size * sizeof(client_connection_t)); + connections->clients = new_array; + if (connections->clients == NULL) { + printf("Failed to allocate memory for array\n"); + } + } + connections->clients[connections->size].client = client; + connections->clients[connections->size].username = username; + connections->size = new_size; + printf("{%llu} Connection added\n", connections->size); + pthread_mutex_unlock(&mutex); + + +}; +void connections_remove(connections_t *connections, SOCKET client) { + if (connections == NULL) { + exit(1); + } + pthread_mutex_lock(&mutex); + + for (int i = 0; i < connections->size; i++) { + if (connections->clients[i].client == client) { + closesocket(connections->clients[i].client); + free(connections->clients[i].username); + for (int j = i; j < connections->size; j++) { + connections->clients[j] = connections->clients[j+1]; + } + connections->size--; + printf("{%llu} Connection removed\n", connections->size); + pthread_mutex_unlock(&mutex); + return; + } + } +} + +void broadcast(const connections_t *connections, const SOCKET *source, const char *message) { + pthread_mutex_lock(&mutex); + for (size_t i = 0; i < connections->size; i++) { + if (connections->clients[i].client != *source) { + send(connections->clients[i].client, message, strlen(message), 0); + } + } + pthread_mutex_unlock(&mutex); +} +char *trim(char *str) { + char *end; + while (isspace(*str)) str++; + if (*str == 0) return str; + end = str + strlen(str) - 1; + while (end > str && isspace(*end)) end--; + end[1] = '\0'; + return str; +} +bool username_is_valid(const char *username) { + if (username == NULL) { + return false; + } + for (int i = 0; i < strlen(username); i++) { + if (!isalnum(username[i])) { + return false; + } + } + return true; +} \ No newline at end of file diff --git a/server03.h b/server03.h new file mode 100644 index 0000000..0fc5f7a --- /dev/null +++ b/server03.h @@ -0,0 +1,42 @@ +// +// Created by Ajurna on 29/07/2025. +// + +#ifndef SERVER03_H +#define SERVER03_H + + +#endif //SERVER03_H +#pragma once +#include + +typedef struct ClientConnection { + SOCKET client; + char *username; +} client_connection_t; + +typedef struct Connections { + size_t size; + size_t len; + client_connection_t *clients; +} connections_t; + +typedef struct handleArgs { + int connection; + SOCKET client; + connections_t connections; +} handle_args_t; + + + +void *handle_connection(void *args); + +connections_t *connections_create(int size); +void connections_destroy(connections_t *connections); +void connections_append(connections_t *connections, SOCKET client, char *username); +void connections_remove(connections_t *connections, SOCKET client); + +char *trim(char *str); +bool username_is_valid(const char *username); + +void broadcast(const connections_t *connections, const SOCKET *source, const char *message); \ No newline at end of file diff --git a/server03_chat_server_tests.py b/server03_chat_server_tests.py new file mode 100644 index 0000000..05d525f --- /dev/null +++ b/server03_chat_server_tests.py @@ -0,0 +1,135 @@ +import socket +import threading +import time +import unittest + +class TestBudgetChat(unittest.TestCase): + SERVER_HOST = 'localhost' + SERVER_PORT = 40000 + + def create_client(self): + """Helper function to create a new client socket""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.SERVER_HOST, self.SERVER_PORT)) + return sock + + def receive_message(self, sock, timeout=2): + """Helper function to receive a message with timeout""" + sock.settimeout(timeout) + try: + return sock.recv(1024).decode('ascii').strip() + except socket.timeout: + return None + + def test_initial_connection(self): + """Test the initial connection and welcome message""" + client = self.create_client() + welcome_msg = self.receive_message(client) + self.assertIsNotNone(welcome_msg) + self.assertTrue("Welcome" in welcome_msg) + client.close() + + def test_invalid_username(self): + """Test that invalid usernames are rejected""" + invalid_names = ["", "user@name", "user name", "!invalid"] + + for name in invalid_names: + with self.subTest(name=name): + client = self.create_client() + _ = self.receive_message(client) # Welcome message + client.send(f"{name}\n".encode('ascii')) + # Server should disconnect us + response = self.receive_message(client) + with self.assertRaises((ConnectionResetError, ConnectionAbortedError, socket.error)): + client.send("test\n".encode('ascii')) + client.close() + + def test_valid_username(self): + """Test that valid usernames are accepted""" + client = self.create_client() + _ = self.receive_message(client) # Welcome message + client.send("validuser123\n".encode('ascii')) + room_info = self.receive_message(client) + self.assertIsNotNone(room_info) + self.assertTrue(room_info.startswith("*")) + client.close() + + def test_chat_message_broadcast(self): + """Test that chat messages are broadcast to other users""" + # First client + client1 = self.create_client() + _ = self.receive_message(client1) # Welcome message + client1.send("user1\n".encode('ascii')) + _ = self.receive_message(client1) # Room info + + # Second client + client2 = self.create_client() + _ = self.receive_message(client2) # Welcome message + client2.send("user2\n".encode('ascii')) + _ = self.receive_message(client2) # Room info + + # User1 should see User2's join message + join_msg = self.receive_message(client1) + self.assertTrue("user2" in join_msg) + + # Send message from user1 + test_message = "Hello, everyone!" + client1.send(f"{test_message}\n".encode('ascii')) + + # User2 should receive the message + received = self.receive_message(client2) + self.assertEqual(f"[user1] {test_message}", received) + + client1.close() + client2.close() + + def test_multiple_clients(self): + """Test that the server can handle multiple clients""" + clients = [] + usernames = [f"user{i}" for i in range(10)] + + # Connect 10 clients + for username in usernames: + client = self.create_client() + _ = self.receive_message(client) # Welcome message + client.send(f"{username}\n".encode('ascii')) + _ = self.receive_message(client) # Room info + clients.append(client) + time.sleep(0.1) # Small delay to prevent race conditions + + # Send a message from the last client + test_message = "Hello from last user!" + clients[-1].send(f"{test_message}\n".encode('ascii')) + + # Check that all other clients received the message + expected = f"[{usernames[-1]}] {test_message}" + for i in range(len(clients)-1): + received = self.receive_message(clients[i]) + self.assertEqual(expected, received) + + # Cleanup + for client in clients: + client.close() + + def test_user_departure(self): + """Test that user departure is announced""" + client1 = self.create_client() + _ = self.receive_message(client1) + client1.send("user1\n".encode('ascii')) + _ = self.receive_message(client1) + + client2 = self.create_client() + _ = self.receive_message(client2) + client2.send("user2\n".encode('ascii')) + _ = self.receive_message(client2) + + # Close client2 and check if client1 receives departure message + client2.close() + departure_msg = self.receive_message(client1) + self.assertTrue("user2" in departure_msg) + self.assertTrue(departure_msg.startswith("*")) + + client1.close() + +if __name__ == '__main__': + unittest.main()