// 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; }