diff --git a/client b/client new file mode 100755 index 0000000..2ac5bec Binary files /dev/null and b/client differ diff --git a/client.c b/client.c new file mode 100644 index 0000000..18129f8 --- /dev/null +++ b/client.c @@ -0,0 +1,383 @@ +#include +#include +#include +#include /* for strncasecmp */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_MESSAGE 1024 +#define MAX_USERNAME 32 +#define MAX_PASSWORD 64 +#define SERVER_IP "127.0.0.1" +#define PORT 8888 + +/* Color palette for structured display */ +#define COLOR_RESET "\033[0m" +#define COLOR_PROMPT "\033[36m" /* cyan */ +#define COLOR_MSG "\033[37m" /* white for normal messages */ +#define COLOR_USER "\033[1;33m" /* bold yellow for usernames */ +#define COLOR_JOIN "\033[32m" /* green for joins */ +#define COLOR_LEAVE "\033[31m" /* red for leaves */ +#define COLOR_SYSTEM "\033[35m" /* magenta for system/info */ +#define COLOR_COMMAND "\033[33m" /* yellow for local commands */ +#define COLOR_ERROR "\033[1;31m" /* bright red for errors */ + +int client_socket; +char username[MAX_USERNAME]; +char current_room[MAX_USERNAME]; + +#define SUPPRESS_WINDOW 2 /* seconds to suppress identical consecutive messages */ + +pthread_mutex_t room_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t socket_mutex = PTHREAD_MUTEX_INITIALIZER; + +/* prevent duplicate consecutive prints (thread-safe) */ +static pthread_mutex_t print_mutex = PTHREAD_MUTEX_INITIALIZER; +static char last_printed[MAX_MESSAGE] = {0}; +static time_t last_print_ts = 0; + +/* helper to rtrim newline characters */ +static void rtrim_newlines(char *s) { + if (!s) return; + size_t len = strlen(s); + while (len > 0 && (s[len-1] == '\n' || s[len-1] == '\r')) { + s[len-1] = '\0'; + len--; + } +} + +/* classify and print a single server message with color */ +static int dedupe_and_store(const char *msg) { + int should_print = 0; + time_t now = time(NULL); + + pthread_mutex_lock(&print_mutex); + if (last_printed[0] == '\0' || strcmp(msg, last_printed) != 0) { + /* new message -> store and allow print */ + strncpy(last_printed, msg, sizeof(last_printed) - 1); + last_printed[sizeof(last_printed) - 1] = '\0'; + last_print_ts = now; + should_print = 1; + } else { + /* identical to last message: only allow if outside suppression window */ + if (difftime(now, last_print_ts) > SUPPRESS_WINDOW) { + last_print_ts = now; + should_print = 1; + } else { + should_print = 0; + } + } + pthread_mutex_unlock(&print_mutex); + return should_print; +} + +static void print_colored_message(const char *msg_in) { + if (!msg_in) return; + char msg[MAX_MESSAGE]; + strncpy(msg, msg_in, sizeof(msg)-1); + msg[sizeof(msg)-1] = '\0'; + rtrim_newlines(msg); + + /* dedupe consecutive identical messages */ + if (!dedupe_and_store(msg)) return; + + /* system responses */ + if (strstr(msg, "You joined room:") || strstr(msg, "Available Rooms:") || + strstr(msg, "Authentication successful") || strstr(msg, "Welcome,") || + strstr(msg, "Registration successful") || strstr(msg, "Server is full") || + strncasecmp(msg, "Error", 5) == 0) { + printf(COLOR_SYSTEM "%s" COLOR_RESET "\n", msg); + return; + } + + /* join / leave notifications */ + if (strstr(msg, " joined the room") || strstr(msg, " joined the chat")) { + printf(COLOR_JOIN "%s" COLOR_RESET "\n", msg); + return; + } + if (strstr(msg, " left the room") || strstr(msg, " left the chat")) { + printf(COLOR_LEAVE "%s" COLOR_RESET "\n", msg); + return; + } + + /* Private message indicator */ + if (strstr(msg, "[PM") || strstr(msg, "PM from") || strstr(msg, "PM to")) { + printf(COLOR_USER "%s" COLOR_RESET "\n", msg); + return; + } + + /* General chat message: try to color username differently when possible */ + /* Expected format: [room] username: message */ + const char *p = strchr(msg, ']'); + const char *colon = strchr(msg, ':'); + if (msg[0] == '[' && p && colon && colon > p) { + /* print prefix ([room]) + username in USER color + rest in MSG color */ + int prefix_len = (int)(p - msg + 1); + char prefix[64] = {0}; + strncpy(prefix, msg, prefix_len); + const char *user_start = p + 2; /* skip "] " */ + int user_len = (int)(colon - user_start); + if (user_len <= 0 || user_len >= 64) { + /* fallback */ + printf(COLOR_MSG "%s" COLOR_RESET "\n", msg); + return; + } + char user[64] = {0}; + strncpy(user, user_start, user_len); + const char *rest = colon + 1; + printf(COLOR_MSG "%s " COLOR_USER "%s" COLOR_MSG ":%s" COLOR_RESET "\n", + prefix, user, rest); + return; + } + + /* default: regular message color */ + printf(COLOR_MSG "%s" COLOR_RESET "\n", msg); +} + +void print_help() { + printf(COLOR_SYSTEM + "=== Available Commands ===\n" + " /rooms - List all available chat rooms\n" + " /join - Join or create a chat room\n" + " /help - Show this help message\n" + " /quit - Exit the chat\n\n" + "Tips:\n" + "- Messages will be sent to your current room\n" + "- Your current room is shown in the prompt [room]>\n" + "- Use /join to switch between rooms\n" + "- Private messages don't work yet\n" + COLOR_RESET); +} + +void clear_input_line() { + printf("\033[2K\r"); // Clear the current line +} + +void show_prompt() { + pthread_mutex_lock(&room_mutex); + printf("\033[2K\r" COLOR_PROMPT "[%s]> " COLOR_RESET, current_room[0] ? current_room : "lobby"); + fflush(stdout); + pthread_mutex_unlock(&room_mutex); +} + +void *receive_messages(void *arg) { + (void)arg; /* silence unused parameter warning */ + char message[MAX_MESSAGE]; + int read_size; + + while ((read_size = recv(client_socket, message, MAX_MESSAGE - 1, 0)) > 0) { + message[read_size] = '\0'; + rtrim_newlines(message); /* remove any trailing newlines sent by server */ + clear_input_line(); /* Clear current input line */ + print_colored_message(message); + show_prompt(); /* Show the prompt again */ + } + + /* server disconnected or error */ + clear_input_line(); + printf(COLOR_ERROR "Disconnected from server." COLOR_RESET "\n"); + show_prompt(); + return NULL; +} + +void hash_password(const char *password, char *hashed, size_t hashed_size) { + (void)hashed_size; /* silence unused parameter warning */ + EVP_MD_CTX *mdctx; + const EVP_MD *md; + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int hash_len; + + // Initialize the hashing context + mdctx = EVP_MD_CTX_new(); + md = EVP_sha256(); + + if (mdctx == NULL) { + fprintf(stderr, "Error creating hash context\n"); + return; + } + + EVP_DigestInit_ex(mdctx, md, NULL); + EVP_DigestUpdate(mdctx, password, strlen(password)); + EVP_DigestFinal_ex(mdctx, hash, &hash_len); + EVP_MD_CTX_free(mdctx); + + // Convert to hex string + for(unsigned int i = 0; i < hash_len; i++) { + snprintf(hashed + (i * 2), 3, "%02x", hash[i]); + } + hashed[hash_len * 2] = '\0'; +} + +int validate_username(const char *username) { + if (!username || strlen(username) < 3 || strlen(username) >= MAX_USERNAME) { + return 0; + } + + for (int i = 0; username[i]; i++) { + if (!isalnum(username[i]) && username[i] != '_') { + return 0; + } + } + return 1; +} + +int validate_password(const char *password) { + if (!password || strlen(password) < 8 || strlen(password) >= MAX_PASSWORD) { + return 0; + } + + int has_upper = 0, has_lower = 0, has_digit = 0; + for (int i = 0; password[i]; i++) { + if (isupper(password[i])) has_upper = 1; + if (islower(password[i])) has_lower = 1; + if (isdigit(password[i])) has_digit = 1; + } + + return has_upper && has_lower && has_digit; +} + +// Add error handling macro +#define HANDLE_ERROR(condition, message) \ + do { \ + if (condition) { \ + fprintf(stderr, "\033[31mError: %s (%s)\033[0m\n", message, strerror(errno)); \ + return 1; \ + } \ + } while(0) + +int main() { + struct sockaddr_in server_addr; + char password[MAX_PASSWORD]; + + char choice[10]; + printf(COLOR_SYSTEM "1. Login\n2. Register\nChoice: " COLOR_RESET); + fgets(choice, sizeof(choice), stdin); + + printf(COLOR_SYSTEM "Username: " COLOR_RESET); + fgets(username, MAX_USERNAME, stdin); + username[strcspn(username, "\n")] = 0; + + printf(COLOR_SYSTEM "Password: " COLOR_RESET); + fgets(password, MAX_PASSWORD, stdin); + password[strcspn(password, "\n")] = 0; + + // Validate username and password + if (!validate_username(username)) { + printf(COLOR_ERROR "Invalid username. Use 3-31 characters, alphanumeric and underscore only." COLOR_RESET "\n"); + return 1; + } + + if (!validate_password(password)) { + printf(COLOR_ERROR "Invalid password. Must be 8+ characters with uppercase, lowercase and numbers." COLOR_RESET "\n"); + return 1; + } + + printf(COLOR_SYSTEM "Connecting to %s:%d...\n" COLOR_RESET, SERVER_IP, PORT); + + client_socket = socket(AF_INET, SOCK_STREAM, 0); + HANDLE_ERROR(client_socket == -1, "Failed to create socket"); + + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(PORT); + + HANDLE_ERROR(inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0, + "Invalid address"); + + HANDLE_ERROR(connect(client_socket, (struct sockaddr *)&server_addr, + sizeof(server_addr)) < 0, "Connection failed"); + + // Send credentials with command + char credentials[MAX_USERNAME + MAX_PASSWORD + 10]; + char hashed_password[65]; + hash_password(password, hashed_password, sizeof(hashed_password)); + + snprintf(credentials, sizeof(credentials), "%s %s %s", + choice[0] == '2' ? "REGISTER" : "LOGIN", + username, hashed_password); + + send(client_socket, credentials, strlen(credentials), 0); + + // Handle response + char response[MAX_MESSAGE]; + ssize_t response_len = recv(client_socket, response, sizeof(response) - 1, 0); + if (response_len <= 0) { + printf(COLOR_ERROR "Server disconnected" COLOR_RESET "\n"); + close(client_socket); + return 1; + } + response[response_len] = '\0'; + rtrim_newlines(response); + print_colored_message(response); + + // If registration, exit after showing response + if (choice[0] == '2') { + close(client_socket); + return 0; + } + + // Continue only if authentication was successful + if (strncmp(response, "Authentication successful", 23) != 0) { + close(client_socket); + return 1; + } + + printf(COLOR_SYSTEM "Successfully connected to server!" COLOR_RESET "\n\n"); + printf(COLOR_SYSTEM "Type /help to see available commands\n\n" COLOR_RESET); + print_help(); + printf("\n"); + + pthread_t receive_thread; + pthread_create(&receive_thread, NULL, receive_messages, NULL); + + char message[MAX_MESSAGE]; + while (1) { + show_prompt(); + if (!fgets(message, MAX_MESSAGE, stdin)) break; + message[strcspn(message, "\n")] = 0; + + if (strcmp(message, "/help") == 0) { + print_help(); + continue; + } + + if (strcmp(message, "/quit") == 0) break; + + if (strncmp(message, "/join ", 6) == 0) { + char *new_room = message + 6; + pthread_mutex_lock(&room_mutex); + strncpy(current_room, new_room, MAX_USERNAME - 1); + current_room[MAX_USERNAME - 1] = '\0'; + pthread_mutex_unlock(&room_mutex); + } + + // Send message + if (strlen(message) > 0) { + pthread_mutex_lock(&socket_mutex); + + // Only print local messages for commands starting with / + if (message[0] == '/') { + /* Local command feedback (colored) */ + if (strcmp(message, "/help") != 0) { /* /help already handled above */ + printf(COLOR_COMMAND "Executing command: %s" COLOR_RESET "\n", message); + } + } + // Send message to server + ssize_t bytes_sent = send(client_socket, message, strlen(message), 0); + if (bytes_sent <= 0) { + printf(COLOR_ERROR "Failed to send message" COLOR_RESET "\n"); + } + + pthread_mutex_unlock(&socket_mutex); + } + } + + close(client_socket); + return 0; +} \ No newline at end of file diff --git a/server b/server new file mode 100755 index 0000000..1026bb9 Binary files /dev/null and b/server differ diff --git a/server.c b/server.c new file mode 100644 index 0000000..7994336 --- /dev/null +++ b/server.c @@ -0,0 +1,839 @@ +// server.c +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_CLIENTS 500 +#define MAX_USERNAME 32 +#define MAX_PASSWORD 64 +#define MAX_MESSAGE 1024 +#define MAX_ROOMS 100 +#define MAX_BANNED_WORDS 50 +#define CONFIG_PATH "server.conf" +#define MAX_MESSAGES_PER_MINUTE 60 + +typedef enum +{ + USER_REGULAR, + USER_MODERATOR, + USER_ADMIN +} UserRole; + +typedef struct +{ + int socket; + char username[MAX_USERNAME]; + char current_room[MAX_USERNAME]; + UserRole role; + time_t last_activity; + int message_count; + time_t last_message_reset; + time_t last_message_time; +} Client; + +typedef struct +{ + char name[MAX_USERNAME]; + char description[256]; + Client *members[MAX_CLIENTS]; + int member_count; + int is_private; + char password[MAX_PASSWORD]; +} Room; + +typedef struct +{ + int port; + char default_room[MAX_USERNAME]; + int max_clients; + int allow_room_creation; + char banned_words[MAX_BANNED_WORDS][32]; + int banned_word_count; + + /* new config options */ + char motd[256]; + char admins[32][MAX_USERNAME]; + int admin_count; + int max_message_length; + int max_rooms; +} ServerConfig; + +sqlite3 *database; +Client clients[MAX_CLIENTS]; +Room rooms[MAX_ROOMS]; +ServerConfig config; +pthread_mutex_t clients_mutex = PTHREAD_MUTEX_INITIALIZER; +int server_socket; +FILE *log_file; +Room global_rooms[MAX_ROOMS] = {0}; +int active_room_count = 0; + +SSL_CTX *ssl_ctx; + +/* Forward declarations to avoid implicit declaration / conflicting type errors */ +void safe_strcpy(char *dest, const char *src, size_t size); +void safe_strcat(char *dest, const char *src, size_t size); + +void server_log(const char *level, const char *message) +{ + time_t now = time(NULL); + char *date = ctime(&now); + date[strlen(date) - 1] = '\0'; + + printf("\033[34m[%s] %s: %s\033[0m\n", date, level, message); + + if (log_file) + { + fprintf(log_file, "[%s] %s: %s\n", date, level, message); + fflush(log_file); + } +} + +int is_word_banned(const char *message) +{ + for (int i = 0; i < config.banned_word_count; i++) + { + if (strcasestr(message, config.banned_words[i])) + { + return 1; + } + } + return 0; +} + +/* small helper: trim leading/trailing whitespace in-place */ +static void trim(char *s) { + if (!s) return; + // trim leading + while (*s && isspace((unsigned char)*s)) memmove(s, s+1, strlen(s)); + // trim trailing + size_t len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len-1])) s[--len] = '\0'; +} + +void parse_config() +{ + /* set sensible defaults */ + strcpy(config.default_room, "lobby"); + config.port = 8888; + config.max_clients = MAX_CLIENTS; + config.allow_room_creation = 1; + config.banned_word_count = 0; + config.motd[0] = '\0'; + config.admin_count = 0; + config.max_message_length = MAX_MESSAGE; /* fallback to compile-time */ + config.max_rooms = MAX_ROOMS; + + FILE *file = fopen(CONFIG_PATH, "r"); + if (!file) + { + server_log("WARN", "No config file found. Using defaults."); + return; + } + + char line[512]; + while (fgets(line, sizeof(line), file)) + { + trim(line); + if (line[0] == '#' || line[0] == ';' || line[0] == '\0') continue; /* comment/empty */ + + char key[128], value[384]; + if (sscanf(line, "%127[^=]=%383[^\n]", key, value) == 2) + { + trim(key); + trim(value); + + if (strcasecmp(key, "port") == 0) { + config.port = atoi(value); + } else if (strcasecmp(key, "default_room") == 0) { + safe_strcpy(config.default_room, value, sizeof(config.default_room)); + } else if (strcasecmp(key, "max_clients") == 0) { + config.max_clients = atoi(value); + if (config.max_clients < 1) config.max_clients = 1; + if (config.max_clients > MAX_CLIENTS) config.max_clients = MAX_CLIENTS; + } else if (strcasecmp(key, "allow_room_creation") == 0) { + config.allow_room_creation = (strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0); + } else if (strcasecmp(key, "banned_words") == 0) { + config.banned_word_count = 0; + char *tok = strtok(value, ","); + while (tok && config.banned_word_count < MAX_BANNED_WORDS) { + trim(tok); + strncpy(config.banned_words[config.banned_word_count++], tok, 31); + config.banned_words[config.banned_word_count-1][31] = '\0'; + tok = strtok(NULL, ","); + } + } else if (strcasecmp(key, "motd") == 0) { + safe_strcpy(config.motd, value, sizeof(config.motd)); + } else if (strcasecmp(key, "admins") == 0) { + config.admin_count = 0; + char *tok = strtok(value, ","); + while (tok && config.admin_count < (int)(sizeof(config.admins)/sizeof(config.admins[0]))) { + trim(tok); + strncpy(config.admins[config.admin_count++], tok, MAX_USERNAME-1); + config.admins[config.admin_count-1][MAX_USERNAME-1] = '\0'; + tok = strtok(NULL, ","); + } + } else if (strcasecmp(key, "max_message_length") == 0) { + config.max_message_length = atoi(value); + if (config.max_message_length < 16) config.max_message_length = 16; + if (config.max_message_length > MAX_MESSAGE) config.max_message_length = MAX_MESSAGE; + } else if (strcasecmp(key, "max_rooms") == 0) { + config.max_rooms = atoi(value); + if (config.max_rooms < 1) config.max_rooms = 1; + if (config.max_rooms > MAX_ROOMS) config.max_rooms = MAX_ROOMS; + } + } + } + fclose(file); + + char buf[256]; + snprintf(buf, sizeof(buf), "Config loaded: port=%d default_room=%s max_clients=%d max_message_length=%d", + config.port, config.default_room, config.max_clients, config.max_message_length); + server_log("INFO", buf); +} + +Room *find_or_create_room(const char *room_name, const char *password, Client *creator) +{ + for (int i = 0; i < active_room_count; i++) + { + if (strcmp(global_rooms[i].name, room_name) == 0) + { + if (global_rooms[i].is_private && strcmp(global_rooms[i].password, password) != 0) + { + return NULL; // Incorrect password + } + return &global_rooms[i]; + } + } + + if (active_room_count < MAX_ROOMS && + (config.allow_room_creation || (creator && creator->role >= USER_MODERATOR))) + { + pthread_mutex_lock(&clients_mutex); /* protect global_rooms modifications */ + Room *new_room = &global_rooms[active_room_count++]; + + /* initialize the new room but do NOT add members here. + Membership must be managed by the caller under clients_mutex + to avoid races/duplicate entries. */ + memset(new_room, 0, sizeof(Room)); + strncpy(new_room->name, room_name, MAX_USERNAME - 1); + new_room->name[MAX_USERNAME - 1] = '\0'; + strncpy(new_room->password, password, MAX_PASSWORD - 1); + new_room->password[MAX_PASSWORD - 1] = '\0'; + new_room->is_private = (password && password[0] != '\0'); + new_room->member_count = 0; + pthread_mutex_unlock(&clients_mutex); + + char log_msg[256]; + snprintf(log_msg, sizeof(log_msg), + "Room created: %s by user %s", + room_name, creator ? creator->username : "system"); + server_log("ROOM", log_msg); + + return new_room; + } + + return NULL; +} + +// Add these safe room membership helpers to avoid duplicates +void add_client_to_room(Room *room, Client *client) { + if (!room || !client) return; + pthread_mutex_lock(&clients_mutex); + // avoid duplicates + for (int i = 0; i < room->member_count; i++) { + if (room->members[i] == client) { + pthread_mutex_unlock(&clients_mutex); + return; + } + } + if (room->member_count < MAX_CLIENTS) { + room->members[room->member_count++] = client; + } + pthread_mutex_unlock(&clients_mutex); +} + +void remove_client_from_room(Room *room, Client *client) { + if (!room || !client) return; + pthread_mutex_lock(&clients_mutex); + for (int i = 0; i < room->member_count; i++) { + if (room->members[i] == client) { + memmove(&room->members[i], &room->members[i + 1], + (room->member_count - i - 1) * sizeof(Client *)); + room->member_count--; + break; + } + } + pthread_mutex_unlock(&clients_mutex); +} + +void broadcast_room_message(Room *room, const char *message, Client *sender) { + if (!room || !sender) return; + + // Don't broadcast commands + if (message[0] == '/') return; + + char formatted_message[MAX_MESSAGE]; + snprintf(formatted_message, sizeof(formatted_message), + "[%s] %s: %s\n", room->name, sender->username, message); + + pthread_mutex_lock(&clients_mutex); + + // First verify sender is still in room + int sender_in_room = 0; + for (int i = 0; i < room->member_count; i++) { + if (room->members[i] == sender && room->members[i]->socket > 0) { + sender_in_room = 1; + break; + } + } + + if (!sender_in_room) { + pthread_mutex_unlock(&clients_mutex); + return; + } + + // Now broadcast to room members (skip sender so messages are not echoed back) + for (int i = 0; i < room->member_count; i++) { + Client *client = room->members[i]; + if (!client || client->socket <= 0) continue; + + // skip duplicates of the same client pointer in the list + int duplicate = 0; + for (int k = 0; k < i; k++) { + if (room->members[k] == client) { duplicate = 1; break; } + } + if (duplicate) continue; + + if (client == sender) continue; /* do not echo to sender */ + + ssize_t sent = send(client->socket, formatted_message, + strlen(formatted_message), MSG_NOSIGNAL); + + if (sent <= 0) { + /* Handle disconnected client: remove from members list while holding mutex */ + if (errno == EPIPE || errno == ECONNRESET || sent == -1) { + close(client->socket); + client->socket = 0; + memmove(&room->members[i], &room->members[i + 1], + (room->member_count - i - 1) * sizeof(Client*)); + room->member_count--; + i--; /* adjust index after removal */ + } + } + } + + pthread_mutex_unlock(&clients_mutex); + server_log("MESSAGE", formatted_message); +} + +int authenticate_user(const char *username, const char *password) +{ + sqlite3_stmt *stmt; + const char *query = "SELECT password FROM users WHERE username = ?"; + + sqlite3_prepare_v2(database, query, -1, &stmt, 0); + sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); + + int authenticated = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) + { + const char *stored_hash = (const char *)sqlite3_column_text(stmt, 0); + const char *computed_hash = crypt(password, stored_hash); + authenticated = (computed_hash && strcmp(computed_hash, stored_hash) == 0); + } + + sqlite3_finalize(stmt); + return authenticated; +} + +int register_user(const char *username, const char *password) +{ + sqlite3_stmt *stmt; + + // Check if user already exists + const char *check_query = "SELECT username FROM users WHERE username = ?"; + sqlite3_prepare_v2(database, check_query, -1, &stmt, 0); + sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); + + if (sqlite3_step(stmt) == SQLITE_ROW) + { + sqlite3_finalize(stmt); + return 0; // User already exists + } + sqlite3_finalize(stmt); + + // Generate salt and hash password + char salt[32]; + snprintf(salt, sizeof(salt), "$6$%08x%08x", rand(), rand()); + char *hashed_password = crypt(password, salt); + + // Insert new user + const char *insert_query = "INSERT INTO users (username, password, role) VALUES (?, ?, ?)"; + sqlite3_prepare_v2(database, insert_query, -1, &stmt, 0); + sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, hashed_password, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 3, USER_REGULAR); + + int result = sqlite3_step(stmt) == SQLITE_DONE; + sqlite3_finalize(stmt); + return result; +} + +void *handle_client(void *arg) +{ + Client *client = (Client *)arg; + char buffer[MAX_MESSAGE]; + int read_size; + + char welcome[MAX_MESSAGE]; + snprintf(welcome, sizeof(welcome), + "Welcome, %s! You are in room %s", + client->username, client->current_room); + send(client->socket, welcome, strlen(welcome), 0); + + // Find default room and add client + Room *default_room = NULL; + for (int i = 0; i < active_room_count; i++) { + if (strcmp(global_rooms[i].name, "lobby") == 0) { + default_room = &global_rooms[i]; + break; + } + } + + if (default_room) { + add_client_to_room(default_room, client); + + strncpy(client->current_room, "lobby", MAX_USERNAME); + + char join_msg[MAX_MESSAGE]; + snprintf(join_msg, sizeof(join_msg), "%s joined the chat", client->username); + broadcast_room_message(default_room, join_msg, client); + + /* send MOTD to the new client if configured */ + if (config.motd[0]) { + char motd_msg[512]; + snprintf(motd_msg, sizeof(motd_msg), "MOTD: %s\n", config.motd); + send(client->socket, motd_msg, strlen(motd_msg), 0); + } + } + + while ((read_size = recv(client->socket, buffer, MAX_MESSAGE, 0)) > 0) + { + /* enforce configured max message length */ + if (config.max_message_length > 0 && read_size > config.max_message_length) { + const char *err = "Your message is too long and was discarded.\n"; + send(client->socket, err, strlen(err), 0); + continue; + } + + buffer[read_size] = '\0'; + + char log_msg[MAX_MESSAGE + 100]; + snprintf(log_msg, sizeof(log_msg), + "Received from %s: %s", client->username, buffer); + server_log("RECV", log_msg); + + // Server: Inside handle_client() + server_log("DEBUG", "Received message"); + printf("Message from %s: %s\n", client->username, buffer); + + if (strncmp(buffer, "/join ", 6) == 0) + { + char room_name[MAX_USERNAME] = {0}; + char password[MAX_PASSWORD] = {0}; + sscanf(buffer + 6, "%s %s", room_name, password); + + Room *room = find_or_create_room(room_name, password, client); + if (room) { + // Remove client from current room (if present) - notify before modifying + Room *old_room = NULL; + int old_index = -1; + for (int i = 0; i < active_room_count; i++) { + if (strcmp(global_rooms[i].name, client->current_room) == 0) { + old_room = &global_rooms[i]; + break; + } + } + + if (old_room) { + for (int j = 0; j < old_room->member_count; j++) { + if (old_room->members[j] == client) { + // Notify old room before removing (don't hold mutex during broadcast) + char leave_msg[MAX_MESSAGE]; + snprintf(leave_msg, sizeof(leave_msg), "%s left the room", client->username); + broadcast_room_message(old_room, leave_msg, client); + + // remove safely using helper + remove_client_from_room(old_room, client); + break; + } + } + } + + // Check if client is already in the new room + int already_in_room = 0; + for (int i = 0; i < room->member_count; i++) { + if (room->members[i] == client) { + already_in_room = 1; + break; + } + } + + if (!already_in_room) { + // Add to new room safely (avoids duplicates) + add_client_to_room(room, client); + + safe_strcpy(client->current_room, room_name, MAX_USERNAME); + + // Notify new room (do not hold mutex while broadcasting) + char join_msg[MAX_MESSAGE]; + snprintf(join_msg, sizeof(join_msg), "%s joined the room", client->username); + broadcast_room_message(room, join_msg, client); + } + + // Send confirmation to client + char confirm_msg[MAX_MESSAGE]; + snprintf(confirm_msg, sizeof(confirm_msg), "You joined room: %s\n", room_name); + send(client->socket, confirm_msg, strlen(confirm_msg), 0); + } else { + const char *error_msg = "Could not join room. It might be private or full.\n"; + send(client->socket, error_msg, strlen(error_msg), 0); + } + } + else if (strcmp(buffer, "/rooms") == 0) + { + char room_list[MAX_MESSAGE] = "Available Rooms:\n"; + for (int i = 0; i < active_room_count; i++) + { + char room_info[128]; + snprintf(room_info, sizeof(room_info), + "- %s (Members: %d)\n", + global_rooms[i].name, global_rooms[i].member_count); + strcat(room_list, room_info); + } + send(client->socket, room_list, strlen(room_list), 0); + } + else + { + Room *current_room = NULL; + for (int i = 0; i < active_room_count; i++) + { + if (strcmp(global_rooms[i].name, client->current_room) == 0) + { + current_room = &global_rooms[i]; + break; + } + } + + if (current_room) + { + if (is_word_banned(buffer)) + { + const char *warning = "Your message contains banned words and was not sent."; + send(client->socket, warning, strlen(warning), 0); + continue; + } + // Server: Before broadcasting + server_log("DEBUG", "Broadcasting message"); + printf("Broadcasting message: %s\n", buffer); + + // Rate limiting + if (time(NULL) - client->last_message_time >= 60) { + client->message_count = 0; + client->last_message_time = time(NULL); + } + + if (client->message_count >= MAX_MESSAGES_PER_MINUTE) { + const char *rate_limit_msg = "Rate limit exceeded. Please wait.\n"; + send(client->socket, rate_limit_msg, strlen(rate_limit_msg), 0); + continue; + } + client->message_count++; + + broadcast_room_message(current_room, buffer, client); + } + } + } + + server_log("DISCONNECT", client->username); + close(client->socket); + client->socket = 0; + return NULL; +} + +void send_private_message(Client *sender, const char *recipient_name, const char *message) { + pthread_mutex_lock(&clients_mutex); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != 0 && strcmp(clients[i].username, recipient_name) == 0) { + char formatted_message[MAX_MESSAGE]; + snprintf(formatted_message, sizeof(formatted_message), + "[PM from %s]: %s", sender->username, message); + send(clients[i].socket, formatted_message, strlen(formatted_message), 0); + + // Send confirmation to sender + snprintf(formatted_message, sizeof(formatted_message), + "[PM to %s]: %s", recipient_name, message); + send(sender->socket, formatted_message, strlen(formatted_message), 0); + pthread_mutex_unlock(&clients_mutex); + return; + } + } + pthread_mutex_unlock(&clients_mutex); + + // User not found + char error_msg[MAX_MESSAGE]; + snprintf(error_msg, sizeof(error_msg), "User '%s' not found or offline.\n", recipient_name); + send(sender->socket, error_msg, strlen(error_msg), 0); +} + +int init_database() +{ + int rc = sqlite3_open("chat.db", &database); + if (rc) + { + server_log("ERROR", "Cannot open database"); + return 0; + } + + const char *tables[] = { + "CREATE TABLE IF NOT EXISTS users (" + "username TEXT PRIMARY KEY, " + "password TEXT, " + "role INTEGER);", + + "CREATE TABLE IF NOT EXISTS rooms (" + "name TEXT PRIMARY KEY," + "description TEXT," + "is_private INTEGER," + "password TEXT);", + + "CREATE TABLE IF NOT EXISTS messages (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "room TEXT," + "sender TEXT," + "content TEXT," + "timestamp INTEGER," + "FOREIGN KEY(room) REFERENCES rooms(name)," + "FOREIGN KEY(sender) REFERENCES users(username));" + }; + + char *err_msg = 0; + for (int i = 0; i < sizeof(tables)/sizeof(tables[0]); i++) { + int rc = sqlite3_exec(database, tables[i], 0, 0, &err_msg); + if (rc != SQLITE_OK) { + char error_message[256]; + snprintf(error_message, sizeof(error_message), "Database error: %s", err_msg); + server_log("ERROR", error_message); + sqlite3_free(err_msg); + return 0; + } + } + + // Create default room if it doesn't exist + const char *default_room_query = + "INSERT OR IGNORE INTO rooms (name, description, is_private) " + "VALUES ('lobby', 'Default chat room', 0);"; + + sqlite3_exec(database, default_room_query, 0, 0, &err_msg); + return 1; +} + +volatile sig_atomic_t server_running = 1; + +void handle_shutdown(int signal) { + server_running = 0; + server_log("INFO", "Shutting down server..."); + + // Close all client connections + pthread_mutex_lock(&clients_mutex); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != 0) { + close(clients[i].socket); + } + } + pthread_mutex_unlock(&clients_mutex); + + // Close server socket + close(server_socket); + + // Close database connection + sqlite3_close(database); + + // Close log file + if (log_file) { + fclose(log_file); + } + + exit(0); +} + +// Add these safe string functions +void safe_strcpy(char *dest, const char *src, size_t size) { + if (!dest || !src || size == 0) return; + strncpy(dest, src, size - 1); + dest[size - 1] = '\0'; +} + +void safe_strcat(char *dest, const char *src, size_t size) { + if (!dest || !src || size == 0) return; + size_t current_len = strlen(dest); + if (current_len >= size - 1) return; + strncpy(dest + current_len, src, size - current_len - 1); + dest[size - 1] = '\0'; +} + +int main() { + srand(time(NULL)); // Initialize random number generator + log_file = fopen("server.log", "a"); + parse_config(); + init_database(); + + struct sockaddr_in server_addr, client_addr; + socklen_t client_addr_len = sizeof(client_addr); + + server_socket = socket(AF_INET, SOCK_STREAM, 0); + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = INADDR_ANY; + server_addr.sin_port = htons(config.port); + + int yes = 1; + if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) { + server_log("ERROR", "Failed to set socket options"); + return 1; + } + + if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + server_log("ERROR", "Bind failed"); + return 1; + } + + if (listen(server_socket, MAX_CLIENTS) < 0) { + server_log("ERROR", "Listen failed"); + return 1; + } + + server_log("INFO", "Server initialized. Waiting for connections..."); + + // Initialize default room + Room *default_room = &global_rooms[active_room_count++]; + strncpy(default_room->name, "lobby", MAX_USERNAME); + default_room->description[0] = '\0'; + default_room->is_private = 0; + default_room->password[0] = '\0'; + default_room->member_count = 0; + + server_log("INFO", "Default room 'lobby' initialized"); + + signal(SIGINT, handle_shutdown); + signal(SIGTERM, handle_shutdown); + + while (server_running) + { + int client_socket = accept(server_socket, + (struct sockaddr *)&client_addr, + &client_addr_len); + + char client_ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip)); + server_log("INFO", "Client connected"); + printf("Client connected from %s:%d\n", client_ip, ntohs(client_addr.sin_port)); + + // Read the combined username and password + char credentials[MAX_USERNAME + MAX_PASSWORD + 10]; // Extra space for command + ssize_t recv_len = recv(client_socket, credentials, sizeof(credentials) - 1, 0); + if (recv_len <= 0) { + close(client_socket); + continue; + } + credentials[recv_len] = '\0'; + + // Parse command, username and password + char command[10]; + char username[MAX_USERNAME]; + char password[MAX_PASSWORD]; + + if (sscanf(credentials, "%9s %31s %63s", command, username, password) != 3) { + const char *error_msg = "Invalid format. Use: LOGIN username password or REGISTER username password\n"; + send(client_socket, error_msg, strlen(error_msg), 0); + close(client_socket); + continue; + } + + if (strcmp(command, "REGISTER") == 0) { + if (register_user(username, password)) { + const char *success_msg = "Registration successful. Please login.\n"; + send(client_socket, success_msg, strlen(success_msg), 0); + } else { + const char *error_msg = "Registration failed. Username may already exist.\n"; + send(client_socket, error_msg, strlen(error_msg), 0); + } + close(client_socket); + continue; + } else if (strcmp(command, "LOGIN") == 0) { + if (authenticate_user(username, password)) { + server_log("INFO", "Authentication successful"); + const char *success_msg = "Authentication successful\n"; + send(client_socket, success_msg, strlen(success_msg), 0); + + pthread_mutex_lock(&clients_mutex); + int slot_found = 0; + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket == 0) { + clients[i].socket = client_socket; + safe_strcpy(clients[i].username, username, MAX_USERNAME); + safe_strcpy(clients[i].current_room, config.default_room, MAX_USERNAME); + clients[i].role = USER_REGULAR; + clients[i].last_activity = time(NULL); + clients[i].message_count = 0; + clients[i].last_message_time = time(NULL); + + pthread_t thread; + if (pthread_create(&thread, NULL, handle_client, &clients[i]) != 0) { + server_log("ERROR", "Failed to create client thread"); + clients[i].socket = 0; + close(client_socket); + } else { + pthread_detach(thread); + slot_found = 1; + } + break; + } + } + pthread_mutex_unlock(&clients_mutex); + + if (!slot_found) { + server_log("ERROR", "Server full"); + const char *error_msg = "Server is full\n"; + send(client_socket, error_msg, strlen(error_msg), 0); + close(client_socket); + } + } else { + server_log("WARN", "Authentication failed"); + const char *error_msg = "Authentication failed: Invalid credentials\n"; + send(client_socket, error_msg, strlen(error_msg), 0); + close(client_socket); + } + } else { + const char *error_msg = "Invalid command. Use LOGIN or REGISTER\n"; + send(client_socket, error_msg, strlen(error_msg), 0); + close(client_socket); + } + } + + sqlite3_close(database); + return 0; +} \ No newline at end of file diff --git a/server.conf b/server.conf new file mode 100644 index 0000000..656ff74 --- /dev/null +++ b/server.conf @@ -0,0 +1,12 @@ +port=8888 +default_room=lobby +max_clients=200 +allow_room_creation=true +max_message_length=1024 +max_rooms=100 + +banned_words=badword,spamword,foo + +admins=Surillya + +motd=Welcome to Suri Chat! Be kind and follow the rules. \ No newline at end of file