init
This commit is contained in:
383
client.c
Normal file
383
client.c
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h> /* for strncasecmp */
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <openssl/err.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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 <room> - 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;
|
||||||
|
}
|
||||||
839
server.c
Normal file
839
server.c
Normal file
@ -0,0 +1,839 @@
|
|||||||
|
// server.c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <crypt.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <openssl/ssl.h>
|
||||||
|
#include <openssl/err.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
12
server.conf
Normal file
12
server.conf
Normal file
@ -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.
|
||||||
Reference in New Issue
Block a user