Add unit test suite and complete protocol documentation

- Add test framework (test.h) with assertion macros and test runner
- Add comprehensive unit tests (test_network.c) covering:
  - Packet parsing and error handling
  - User registration and authentication
  - Room operations (create, join, leave, delete, list)
  - Direct messaging functionality
  - Message broadcasting
  - Password hashing
- Update Makefile with 'make test' target
- Rewrite PROTOCOL file with complete specification:
  - All 14 packet types with data layouts
  - All error codes with descriptions
  - Typical usage flows
This commit is contained in:
Lorenzo Torres 2026-01-08 15:38:28 +01:00
parent 670e38a105
commit 238eff5bb3
9 changed files with 2156 additions and 27 deletions

View file

@ -30,8 +30,17 @@ users.h:
asfur: ${OBJ}
${CC} -o $@ ${OBJ} ${LDFLAGS}
test: test_network
./test_network
test_network: test_network.o network.o password.o
${CC} -o $@ test_network.o network.o password.o ${LDFLAGS}
test_network.o: test_network.c test.h network.h client.h password.h
${CC} -c ${CFLAGS} test_network.c
clean:
rm -f asfur ${OBJ} asfur-${VERSION}.tar.gz
rm -f asfur test_network ${OBJ} test_network.o asfur-${VERSION}.tar.gz
dist: clean
mkdir -p asfur-${VERSION}
@ -53,4 +62,4 @@ uninstall:
rm -f ${DESTDIR}${PREFIX}/bin/asfur\
${DESTDIR}${MANPREFIX}/man1/asfur.1
.PHONY: all options clean dist install uninstall
.PHONY: all options clean dist install uninstall test

195
PROTOCOL
View file

@ -1,20 +1,177 @@
for client:
server parses packets from left to right like this:
[u16 length][u8 type][char *data]
ASFUR PROTOCOL SPECIFICATION
length: the length of the entire packet
type:
PACKET_ERROR = 0
PACKET_REGISTER = 1
PACKET_AUTHENTICATE = 2
PACKET_JOIN = 3
PACKET_TEXT = 4
PACKET_LEAVE = 5
data: bytes of data, what it could represent depends on the packet type
(following layouts are layouts for data byte array)
PACKET_REGISTER : [char username[20]][char password[20]]
PACKET_AUTHENTICATE : [char username[20]][char password[20]]
PACKET_JOIN : []
PACKET_TEXT :
PACKET_LEAVE :
All packets follow this structure:
[u16 size][u8 type][data...]
size: total length of the packet (including header)
type: packet type identifier
data: payload bytes (format depends on type)
================================================================================
PACKET TYPES
================================================================================
Client -> Server:
PACKET_REGISTER = 1
PACKET_AUTHENTICATE = 2
PACKET_JOIN = 3
PACKET_TEXT = 4
PACKET_LEAVE = 5
PACKET_DM_OPEN = 7
PACKET_CREATE_ROOM = 9
PACKET_DELETE_ROOM = 10
PACKET_LIST_ROOMS = 12
Server -> Client:
PACKET_ERROR = 0
PACKET_OK = 6
PACKET_DM_ROOM = 8
PACKET_ROOM_CREATED = 11
PACKET_ROOM_LIST = 13
Bidirectional:
PACKET_TEXT = 4 (client sends, server broadcasts)
================================================================================
PACKET DATA LAYOUTS
================================================================================
PACKET_ERROR (0)
Server response indicating an error occurred.
[u8 code]
Error codes:
0 = ERR_OK (no error)
1 = ERR_UNKNOWN (unknown error)
2 = ERR_INVALID_PACKET (malformed packet)
3 = ERR_NOT_AUTHENTICATED (action requires authentication)
4 = ERR_ALREADY_REGISTERED (username already taken)
5 = ERR_INVALID_CREDENTIALS (wrong username/password)
6 = ERR_REGISTRATION_DISABLED (server disabled registration)
7 = ERR_DATABASE (internal database error)
8 = ERR_USER_NOT_FOUND (target user does not exist)
9 = ERR_ACCESS_DENIED (not allowed to access resource)
10 = ERR_ROOM_NOT_FOUND (room does not exist)
11 = ERR_ROOM_NAME_TAKEN (room name already in use)
12 = ERR_NOT_ROOM_OWNER (action requires room ownership)
PACKET_REGISTER (1)
Register a new user account.
[char username[20]][char password[100]]
Response: PACKET_OK or PACKET_ERROR
PACKET_AUTHENTICATE (2)
Authenticate with existing credentials.
[char username[20]][char password[100]]
Response: PACKET_OK or PACKET_ERROR
PACKET_JOIN (3)
Join a room (public or DM).
[u64 room_id]
Response: PACKET_OK or PACKET_ERROR
Requires: authentication
PACKET_TEXT (4)
Send a message to a room (client -> server).
[u64 room_id][char message[...]]
message is variable length (packet size - header - 8 bytes)
Client must have joined the room first.
Requires: authentication, joined room
Broadcast format (server -> client):
[u64 room_id][char username[32]][char message[...]]
Server broadcasts to all authenticated clients in the room.
PACKET_LEAVE (5)
Leave a room.
[u64 room_id]
Response: PACKET_OK or PACKET_ERROR
Requires: authentication
PACKET_OK (6)
Server response indicating success.
(no data)
PACKET_DM_OPEN (7)
Open or get existing DM room with another user.
[char username[20]]
Response: PACKET_DM_ROOM or PACKET_ERROR
Requires: authentication
PACKET_DM_ROOM (8)
Server response with DM room ID.
[u64 room_id]
The room_id can be used with PACKET_JOIN.
PACKET_CREATE_ROOM (9)
Create a new public room.
[char name[32]]
Response: PACKET_ROOM_CREATED or PACKET_ERROR
Requires: authentication
PACKET_DELETE_ROOM (10)
Delete a public room.
[u64 room_id]
Response: PACKET_OK or PACKET_ERROR
Requires: authentication, room ownership
PACKET_ROOM_CREATED (11)
Server response after successful room creation.
[u64 room_id][char name[32]]
PACKET_LIST_ROOMS (12)
Request list of all public rooms.
(no data)
Response: PACKET_ROOM_LIST or PACKET_ERROR
Requires: authentication
PACKET_ROOM_LIST (13)
Server response with list of public rooms.
[u32 count][room_entry[count]]
room_entry format:
[u64 room_id][char name[32]][char owner[20]]
================================================================================
TYPICAL FLOWS
================================================================================
Registration:
Client: PACKET_REGISTER(username, password)
Server: PACKET_OK or PACKET_ERROR(ERR_ALREADY_REGISTERED)
Authentication:
Client: PACKET_AUTHENTICATE(username, password)
Server: PACKET_OK or PACKET_ERROR(ERR_INVALID_CREDENTIALS)
Join public room:
Client: PACKET_LIST_ROOMS
Server: PACKET_ROOM_LIST(rooms)
Client: PACKET_JOIN(room_id)
Server: PACKET_OK
Send message:
Client: PACKET_TEXT(room_id, "hello")
Server: PACKET_TEXT(room_id, sender_username, "hello") -> all clients in room
Direct message:
Client: PACKET_DM_OPEN(target_username)
Server: PACKET_DM_ROOM(room_id)
Client: PACKET_JOIN(room_id)
Server: PACKET_OK
Client: PACKET_TEXT(room_id, "private message")
Create room:
Client: PACKET_CREATE_ROOM("my-room")
Server: PACKET_ROOM_CREATED(room_id, "my-room")

View file

@ -4,3 +4,4 @@
#define CONFIG_CERT_FILE "cert.pem"
#define CONFIG_KEY_FILE "key.pem"
#define CONFIG_DB_PATH "asfur.db"

View file

@ -12,7 +12,7 @@ MANPREFIX = ${PREFIX}/share/man
# includes and libs
INCS = -I.
LIBS = -lssl -lcrypto -lcrypt -luv
LIBS = -lssl -lcrypto -lcrypt -luv -lsqlite3
# flags
CFLAGS := -std=c11 -pedantic -Wall -O0 ${INCS} -DVERSION=\"${VERSION}\"
CFLAGS := ${CFLAGS} -g

23
main.c
View file

@ -5,7 +5,9 @@
#include <uv.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include "config.h"
#include "client.h"
#include "network.h"
uv_loop_t *loop;
SSL_CTX *ctx;
@ -13,6 +15,7 @@ SSL_CTX *ctx;
void cleanup_client(uv_handle_t *handle)
{
struct client *client = (struct client*) handle;
network_client_remove(client);
if (client->ssl) {
SSL_free(client->ssl);
}
@ -65,11 +68,11 @@ void on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf)
char plain_buf[4096];
int p;
while ((p = SSL_read(client->ssl, plain_buf, sizeof(plain_buf))) > 0) {
ssl_write_msg(client, plain_buf, p);
network_handle_data(client, plain_buf, p);
}
flush_ssl_to_socket(client);
}
else if (nread < 0) {
@ -102,7 +105,13 @@ void on_new_connection(uv_stream_t *server, int status)
client->wbio = BIO_new(BIO_s_mem());
SSL_set_bio(client->ssl, client->rbio, client->wbio);
SSL_set_accept_state(client->ssl);
client->user_id = 0;
client->current_room_id = 0;
client->is_authenticated = false;
client->username[0] = '\0';
network_client_add(client);
uv_read_start((uv_stream_t*) &client->handle, alloc_buffer, on_read);
} else {
uv_close((uv_handle_t*) &client->handle, cleanup_client);
@ -132,6 +141,12 @@ void init_openssl()
int main()
{
init_openssl();
if (network_init() != 0) {
fprintf(stderr, "Failed to initialize network/database\n");
return 1;
}
loop = uv_default_loop();
uv_tcp_t server;

673
network.c
View file

@ -1,7 +1,678 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sqlite3.h>
#include "network.h"
#include "client.h"
#include "password.h"
#include "config.h"
void network_parse_packet()
#define MAX_CLIENTS 1024
static sqlite3 *db = NULL;
static struct client *clients[MAX_CLIENTS];
static int client_count = 0;
static const char *schema_sql =
"CREATE TABLE IF NOT EXISTS users ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" username TEXT UNIQUE NOT NULL,"
" password_hash TEXT NOT NULL,"
" created_at INTEGER DEFAULT (strftime('%s', 'now'))"
");"
"CREATE TABLE IF NOT EXISTS dm_rooms ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" user1_id INTEGER NOT NULL,"
" user2_id INTEGER NOT NULL,"
" created_at INTEGER DEFAULT (strftime('%s', 'now')),"
" UNIQUE(user1_id, user2_id),"
" FOREIGN KEY(user1_id) REFERENCES users(id),"
" FOREIGN KEY(user2_id) REFERENCES users(id)"
");"
"CREATE TABLE IF NOT EXISTS rooms ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT UNIQUE NOT NULL,"
" owner_id INTEGER NOT NULL,"
" created_at INTEGER DEFAULT (strftime('%s', 'now')),"
" FOREIGN KEY(owner_id) REFERENCES users(id)"
");";
int network_init(void)
{
int rc = sqlite3_open(CONFIG_DB_PATH, &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
return -1;
}
char *err_msg = NULL;
rc = sqlite3_exec(db, schema_sql, NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
return -1;
}
return 0;
}
void network_shutdown(void)
{
if (db) {
sqlite3_close(db);
db = NULL;
}
}
void network_client_add(struct client *client)
{
if (client_count >= MAX_CLIENTS) return;
clients[client_count++] = client;
}
void network_client_remove(struct client *client)
{
for (int i = 0; i < client_count; i++) {
if (clients[i] == client) {
clients[i] = clients[--client_count];
return;
}
}
}
static void send_error(struct client *client, error_code code);
static void send_ok(struct client *client);
static int is_dm_room_member(uint64_t room_id, uint32_t user_id);
static int is_public_room(uint64_t room_id);
extern void ssl_write_msg(struct client *client, const char *data, size_t len);
static void send_packet(struct client *client, uint8_t type, const void *data, size_t data_len)
{
size_t total_len = sizeof(struct packet_header) + data_len;
char *buf = malloc(total_len);
if (!buf) return;
struct packet_header *hdr = (struct packet_header *)buf;
hdr->size = (uint16_t)total_len;
hdr->type = type;
if (data && data_len > 0) {
memcpy(buf + sizeof(struct packet_header), data, data_len);
}
ssl_write_msg(client, buf, total_len);
free(buf);
}
static void send_error(struct client *client, error_code code)
{
struct packet_error err = { .code = (uint8_t)code };
send_packet(client, PACKET_ERROR, &err, sizeof(err));
}
static void send_ok(struct client *client)
{
send_packet(client, PACKET_OK, NULL, 0);
}
static int handle_register(struct client *client, const char *data, size_t len)
{
if (len < sizeof(struct packet_register)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
#ifndef CONFIG_ALLOW_REGISTER
send_error(client, ERR_REGISTRATION_DISABLED);
return -1;
#else
if (!CONFIG_ALLOW_REGISTER) {
send_error(client, ERR_REGISTRATION_DISABLED);
return -1;
}
#endif
const struct packet_register *reg = (const struct packet_register *)data;
char username[21] = {0};
char password[101] = {0};
memcpy(username, reg->username, 20);
memcpy(password, reg->password, 100);
if (strlen(username) == 0 || strlen(password) == 0) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
char *hash = hash_password(password);
if (!hash) {
send_error(client, ERR_DATABASE);
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "INSERT INTO users (username, password_hash) VALUES (?, ?);";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
free(hash);
send_error(client, ERR_DATABASE);
return -1;
}
sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, hash, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
free(hash);
if (rc == SQLITE_CONSTRAINT) {
send_error(client, ERR_ALREADY_REGISTERED);
return -1;
} else if (rc != SQLITE_DONE) {
send_error(client, ERR_DATABASE);
return -1;
}
send_ok(client);
return 0;
}
static int handle_authenticate(struct client *client, const char *data, size_t len)
{
if (len < sizeof(struct packet_auth)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_auth *auth = (const struct packet_auth *)data;
char username[21] = {0};
char password[101] = {0};
memcpy(username, auth->username, 20);
memcpy(password, auth->password, 100);
if (strlen(username) == 0 || strlen(password) == 0) {
send_error(client, ERR_INVALID_CREDENTIALS);
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT id, password_hash FROM users WHERE username = ?;";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
send_error(client, ERR_DATABASE);
return -1;
}
sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
send_error(client, ERR_INVALID_CREDENTIALS);
return -1;
}
int user_id = sqlite3_column_int(stmt, 0);
const char *stored_hash = (const char *)sqlite3_column_text(stmt, 1);
if (!verify_password(password, stored_hash)) {
sqlite3_finalize(stmt);
send_error(client, ERR_INVALID_CREDENTIALS);
return -1;
}
sqlite3_finalize(stmt);
client->user_id = user_id;
client->is_authenticated = true;
strncpy(client->username, username, sizeof(client->username) - 1);
client->username[sizeof(client->username) - 1] = '\0';
send_ok(client);
return 0;
}
static int handle_join(struct client *client, const char *data, size_t len)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
if (len < sizeof(struct packet_join)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_join *join = (const struct packet_join *)data;
if (is_public_room(join->room_id)) {
client->current_room_id = join->room_id;
send_ok(client);
return 0;
}
if (is_dm_room_member(join->room_id, client->user_id)) {
client->current_room_id = join->room_id;
send_ok(client);
return 0;
}
send_error(client, ERR_ACCESS_DENIED);
return -1;
}
static int handle_leave(struct client *client, const char *data, size_t len)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
if (len < sizeof(struct packet_leave)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_leave *leave = (const struct packet_leave *)data;
if (client->current_room_id == leave->room_id) {
client->current_room_id = 0;
}
send_ok(client);
return 0;
}
static int64_t get_or_create_dm_room(uint32_t user1_id, uint32_t user2_id)
{
uint32_t low_id = user1_id < user2_id ? user1_id : user2_id;
uint32_t high_id = user1_id < user2_id ? user2_id : user1_id;
sqlite3_stmt *stmt;
const char *select_sql = "SELECT id FROM dm_rooms WHERE user1_id = ? AND user2_id = ?;";
int rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, low_id);
sqlite3_bind_int(stmt, 2, high_id);
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
int64_t room_id = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return room_id;
}
sqlite3_finalize(stmt);
const char *insert_sql = "INSERT INTO dm_rooms (user1_id, user2_id) VALUES (?, ?);";
rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, low_id);
sqlite3_bind_int(stmt, 2, high_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) return -1;
return sqlite3_last_insert_rowid(db);
}
static int is_dm_room_member(uint64_t room_id, uint32_t user_id)
{
sqlite3_stmt *stmt;
const char *sql = "SELECT 1 FROM dm_rooms WHERE id = ? AND (user1_id = ? OR user2_id = ?);";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return 0;
sqlite3_bind_int64(stmt, 1, room_id);
sqlite3_bind_int(stmt, 2, user_id);
sqlite3_bind_int(stmt, 3, user_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_ROW;
}
static int is_public_room(uint64_t room_id)
{
sqlite3_stmt *stmt;
const char *sql = "SELECT 1 FROM rooms WHERE id = ?;";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return 0;
sqlite3_bind_int64(stmt, 1, room_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_ROW;
}
static int is_room_owner(uint64_t room_id, uint32_t user_id)
{
sqlite3_stmt *stmt;
const char *sql = "SELECT 1 FROM rooms WHERE id = ? AND owner_id = ?;";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return 0;
sqlite3_bind_int64(stmt, 1, room_id);
sqlite3_bind_int(stmt, 2, user_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_ROW;
}
static int handle_create_room(struct client *client, const char *data, size_t len)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
if (len < sizeof(struct packet_create_room)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_create_room *create = (const struct packet_create_room *)data;
char name[33] = {0};
memcpy(name, create->name, 32);
if (strlen(name) == 0) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "INSERT INTO rooms (name, owner_id) VALUES (?, ?);";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
send_error(client, ERR_DATABASE);
return -1;
}
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 2, client->user_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc == SQLITE_CONSTRAINT) {
send_error(client, ERR_ROOM_NAME_TAKEN);
return -1;
} else if (rc != SQLITE_DONE) {
send_error(client, ERR_DATABASE);
return -1;
}
int64_t room_id = sqlite3_last_insert_rowid(db);
struct packet_room_created response;
response.room_id = (uint64_t)room_id;
memset(response.name, 0, sizeof(response.name));
strncpy(response.name, name, sizeof(response.name) - 1);
send_packet(client, PACKET_ROOM_CREATED, &response, sizeof(response));
return 0;
}
static int handle_delete_room(struct client *client, const char *data, size_t len)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
if (len < sizeof(struct packet_delete_room)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_delete_room *del = (const struct packet_delete_room *)data;
if (!is_public_room(del->room_id)) {
send_error(client, ERR_ROOM_NOT_FOUND);
return -1;
}
if (!is_room_owner(del->room_id, client->user_id)) {
send_error(client, ERR_NOT_ROOM_OWNER);
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "DELETE FROM rooms WHERE id = ?;";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
send_error(client, ERR_DATABASE);
return -1;
}
sqlite3_bind_int64(stmt, 1, del->room_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
send_error(client, ERR_DATABASE);
return -1;
}
for (int i = 0; i < client_count; i++) {
if (clients[i]->current_room_id == del->room_id) {
clients[i]->current_room_id = 0;
}
}
send_ok(client);
return 0;
}
static int handle_list_rooms(struct client *client)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT r.id, r.name, u.username FROM rooms r "
"JOIN users u ON r.owner_id = u.id ORDER BY r.name;";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
send_error(client, ERR_DATABASE);
return -1;
}
struct room_list_entry entries[256];
uint32_t count = 0;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && count < 256) {
entries[count].room_id = sqlite3_column_int64(stmt, 0);
memset(entries[count].name, 0, sizeof(entries[count].name));
const char *name = (const char *)sqlite3_column_text(stmt, 1);
if (name) strncpy(entries[count].name, name, 31);
memset(entries[count].owner, 0, sizeof(entries[count].owner));
const char *owner = (const char *)sqlite3_column_text(stmt, 2);
if (owner) strncpy(entries[count].owner, owner, 19);
count++;
}
sqlite3_finalize(stmt);
size_t data_len = sizeof(uint32_t) + count * sizeof(struct room_list_entry);
char *data = malloc(data_len);
if (!data) {
send_error(client, ERR_DATABASE);
return -1;
}
memcpy(data, &count, sizeof(uint32_t));
memcpy(data + sizeof(uint32_t), entries, count * sizeof(struct room_list_entry));
send_packet(client, PACKET_ROOM_LIST, data, data_len);
free(data);
return 0;
}
static int handle_dm_open(struct client *client, const char *data, size_t len)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
if (len < sizeof(struct packet_dm_open)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_dm_open *dm = (const struct packet_dm_open *)data;
char target_username[21] = {0};
memcpy(target_username, dm->username, 20);
if (strlen(target_username) == 0) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT id FROM users WHERE username = ?;";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
send_error(client, ERR_DATABASE);
return -1;
}
sqlite3_bind_text(stmt, 1, target_username, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
send_error(client, ERR_USER_NOT_FOUND);
return -1;
}
uint32_t target_user_id = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
int64_t room_id = get_or_create_dm_room(client->user_id, target_user_id);
if (room_id < 0) {
send_error(client, ERR_DATABASE);
return -1;
}
struct packet_dm_room response = { .room_id = (uint64_t)room_id };
send_packet(client, PACKET_DM_ROOM, &response, sizeof(response));
return 0;
}
static void broadcast_to_room(uint64_t room_id, const char *username,
const char *message, size_t msg_len)
{
size_t bcast_len = sizeof(uint64_t) + 32 + msg_len;
char *bcast_data = malloc(bcast_len);
if (!bcast_data) return;
memcpy(bcast_data, &room_id, sizeof(uint64_t));
memset(bcast_data + sizeof(uint64_t), 0, 32);
strncpy(bcast_data + sizeof(uint64_t), username, 31);
memcpy(bcast_data + sizeof(uint64_t) + 32, message, msg_len);
for (int i = 0; i < client_count; i++) {
struct client *c = clients[i];
if (c->is_authenticated && c->current_room_id == room_id) {
send_packet(c, PACKET_TEXT, bcast_data, bcast_len);
}
}
free(bcast_data);
}
static int handle_text(struct client *client, const char *data, size_t len)
{
if (!client->is_authenticated) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
if (len < sizeof(uint64_t)) {
send_error(client, ERR_INVALID_PACKET);
return -1;
}
const struct packet_text *text = (const struct packet_text *)data;
size_t msg_len = len - sizeof(uint64_t);
if (client->current_room_id != text->room_id) {
send_error(client, ERR_NOT_AUTHENTICATED);
return -1;
}
broadcast_to_room(text->room_id, client->username, text->message, msg_len);
return 0;
}
void network_handle_data(struct client *client, const char *data, size_t len)
{
if (len < sizeof(struct packet_header)) {
send_error(client, ERR_INVALID_PACKET);
return;
}
const struct packet_header *hdr = (const struct packet_header *)data;
if (hdr->size > len || hdr->size < sizeof(struct packet_header)) {
send_error(client, ERR_INVALID_PACKET);
return;
}
const char *payload = data + sizeof(struct packet_header);
size_t payload_len = hdr->size - sizeof(struct packet_header);
switch (hdr->type) {
case PACKET_REGISTER:
handle_register(client, payload, payload_len);
break;
case PACKET_AUTHENTICATE:
handle_authenticate(client, payload, payload_len);
break;
case PACKET_JOIN:
handle_join(client, payload, payload_len);
break;
case PACKET_LEAVE:
handle_leave(client, payload, payload_len);
break;
case PACKET_TEXT:
handle_text(client, payload, payload_len);
break;
case PACKET_DM_OPEN:
handle_dm_open(client, payload, payload_len);
break;
case PACKET_CREATE_ROOM:
handle_create_room(client, payload, payload_len);
break;
case PACKET_DELETE_ROOM:
handle_delete_room(client, payload, payload_len);
break;
case PACKET_LIST_ROOMS:
handle_list_rooms(client);
break;
default:
send_error(client, ERR_INVALID_PACKET);
break;
}
}

106
network.h
View file

@ -3,9 +3,115 @@
#include <stdint.h>
struct client;
typedef enum {
PACKET_ERROR = 0,
PACKET_REGISTER = 1,
PACKET_AUTHENTICATE = 2,
PACKET_JOIN = 3,
PACKET_TEXT = 4,
PACKET_LEAVE = 5,
PACKET_OK = 6,
PACKET_DM_OPEN = 7,
PACKET_DM_ROOM = 8,
PACKET_CREATE_ROOM = 9,
PACKET_DELETE_ROOM = 10,
PACKET_ROOM_CREATED = 11,
PACKET_LIST_ROOMS = 12,
PACKET_ROOM_LIST = 13
} packet_type;
typedef enum {
ERR_OK = 0,
ERR_UNKNOWN = 1,
ERR_INVALID_PACKET = 2,
ERR_NOT_AUTHENTICATED = 3,
ERR_ALREADY_REGISTERED = 4,
ERR_INVALID_CREDENTIALS = 5,
ERR_REGISTRATION_DISABLED = 6,
ERR_DATABASE = 7,
ERR_USER_NOT_FOUND = 8,
ERR_ACCESS_DENIED = 9,
ERR_ROOM_NOT_FOUND = 10,
ERR_ROOM_NAME_TAKEN = 11,
ERR_NOT_ROOM_OWNER = 12
} error_code;
struct packet_header {
uint16_t size;
uint8_t type;
};
struct packet_register {
char username[20];
char password[100];
};
struct packet_auth {
char username[20];
char password[100];
};
struct packet_join {
uint64_t room_id;
};
struct packet_leave {
uint64_t room_id;
};
struct packet_text {
uint64_t room_id;
char message[];
};
struct packet_error {
uint8_t code;
};
struct packet_text_broadcast {
uint64_t room_id;
char username[32];
char message[];
};
struct packet_dm_open {
char username[20];
};
struct packet_dm_room {
uint64_t room_id;
};
struct packet_create_room {
char name[32];
};
struct packet_delete_room {
uint64_t room_id;
};
struct packet_room_created {
uint64_t room_id;
char name[32];
};
struct room_list_entry {
uint64_t room_id;
char name[32];
char owner[20];
};
struct packet_room_list {
uint32_t count;
struct room_list_entry rooms[];
};
int network_init(void);
void network_shutdown(void);
void network_handle_data(struct client *client, const char *data, size_t len);
void network_client_add(struct client *client);
void network_client_remove(struct client *client);
#endif

53
test.h Normal file
View file

@ -0,0 +1,53 @@
/* Simple unit test framework for asfur */
#ifndef TEST_H
#define TEST_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;
#define TEST_ASSERT(cond, msg) do { \
if (!(cond)) { \
printf(" FAIL: %s\n", msg); \
return 1; \
} \
} while(0)
#define TEST_ASSERT_EQ(a, b, msg) do { \
if ((a) != (b)) { \
printf(" FAIL: %s (expected %d, got %d)\n", msg, (int)(b), (int)(a)); \
return 1; \
} \
} while(0)
#define TEST_ASSERT_STR_EQ(a, b, msg) do { \
if (strcmp((a), (b)) != 0) { \
printf(" FAIL: %s (expected '%s', got '%s')\n", msg, (b), (a)); \
return 1; \
} \
} while(0)
#define RUN_TEST(test_fn) do { \
tests_run++; \
printf("Running %s...\n", #test_fn); \
if (test_fn() == 0) { \
printf(" PASS\n"); \
tests_passed++; \
} else { \
tests_failed++; \
} \
} while(0)
#define TEST_SUMMARY() do { \
printf("\n=== Test Summary ===\n"); \
printf("Total: %d\n", tests_run); \
printf("Passed: %d\n", tests_passed); \
printf("Failed: %d\n", tests_failed); \
return tests_failed > 0 ? 1 : 0; \
} while(0)
#endif

1117
test_network.c Normal file

File diff suppressed because it is too large Load diff