Compare commits

...

13 Commits

Author SHA1 Message Date
db861cd698 panel and scroll_view 2026-01-16 19:35:11 +05:00
b9a622c9d1 turned ClientCLI to a state machine 2026-01-13 18:44:38 +05:00
75891025d0 xterm256 colors 2026-01-13 13:06:32 +05:00
d68ce8d4f0 changed .vscode/launch.json 2026-01-13 11:17:19 +05:00
b081e52d6e fixed visual bugs 2026-01-12 23:06:00 +05:00
310e4867d5 rewrite askUserNameAndPassword() to use tim 2026-01-12 23:00:01 +05:00
bfcb2f931f tim updated 2026-01-09 11:13:55 +05:00
151ad13853 added dependency: tim 2026-01-09 06:12:17 +05:00
0132e71c88 added methods for Client to send and receive messages 2026-01-04 00:53:35 +05:00
90e21bc5ae merged all public headers into single tcp-chat.h 2025-12-23 00:43:11 +05:00
e2edd4070a implemented ClientCLI DB queries 2025-12-23 00:27:02 +05:00
d461cae077 implemented server database and api for client database 2025-12-21 20:29:35 +05:00
49793e2929 implemented CommonQueries 2025-12-15 23:26:32 +05:00
56 changed files with 1877 additions and 1071 deletions

3
.gitmodules vendored
View File

@@ -10,3 +10,6 @@
[submodule "dependencies/tsqlite"]
path = dependencies/tsqlite
url = https://timerix.ddns.net/git/Timerix/tsqlite.git
[submodule "dependencies/tim"]
path = dependencies/tim
url = https://timerix.ddns.net/git/Timerix/tim.git

View File

@@ -9,6 +9,8 @@
"dependencies/BearSSL/inc",
"dependencies/tlibc/include",
"dependencies/tlibtoml/include",
"dependencies/tsqlite/include",
"dependencies/tim/include",
"${default}"
],
"cStandard": "c99"

72
.vscode/launch.json vendored
View File

@@ -2,29 +2,71 @@
"version": "0.2.0",
"configurations": [
{
"name": "gdb_debug",
"name": "(gdb) Client | Build and debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": { "program": "${workspaceFolder}/bin/tcp-chat.exe" },
// "args": [ "-l" ],
"preLaunchTask": "build_exec_dbg",
"stopAtEntry": false,
"cwd": "${workspaceFolder}/bin",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": {
"program": "${workspaceFolder}/bin/tcp-chat.exe",
"externalConsole": true
},
"preLaunchTask": "build_exec_dbg",
"stopAtEntry": false,
"externalConsole": false,
"internalConsoleOptions": "neverOpen",
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
"miDebuggerPath": "gdb"
},
{
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
"name": "(gdb) Client | Just debug",
"type": "cppdbg",
"request": "launch",
"cwd": "${workspaceFolder}/bin",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": {
"program": "${workspaceFolder}/bin/tcp-chat.exe",
"externalConsole": true
},
"stopAtEntry": false,
"externalConsole": false,
"internalConsoleOptions": "neverOpen",
"MIMode": "gdb",
"miDebuggerPath": "gdb"
},
{
"name": "(gdb) Server | Build and debug",
"type": "cppdbg",
"request": "launch",
"cwd": "${workspaceFolder}/bin",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": { "program": "${workspaceFolder}/bin/tcp-chat.exe" },
"args": [ "-l" ],
"preLaunchTask": "build_exec_dbg",
"stopAtEntry": false,
"externalConsole": false,
"internalConsoleOptions": "neverOpen",
"MIMode": "gdb",
"miDebuggerPath": "gdb"
},
{
"name": "(gdb) Server | Just debug",
"type": "cppdbg",
"request": "launch",
"cwd": "${workspaceFolder}/bin",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": { "program": "${workspaceFolder}/bin/tcp-chat.exe" },
"args": [ "-l" ],
"stopAtEntry": false,
"externalConsole": false,
"internalConsoleOptions": "neverOpen",
"MIMode": "gdb",
"miDebuggerPath": "gdb"
}
]
}

1
dependencies/tim vendored Submodule

Submodule dependencies/tim added at ee6375f553

19
dependencies/tim.config vendored Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# This is a dependency config.
# You can copy it to another cbuild project to add this lib as dependency.
DEP_WORKING_DIR="$DEPENDENCIES_DIR/tim"
if [[ "$TASK" = *_dbg ]]; then
dep_build_target="build_static_lib_dbg"
else
dep_build_target="build_static_lib"
fi
DEP_PRE_BUILD_COMMAND=""
DEP_BUILD_COMMAND="cbuild $dep_build_target"
DEP_POST_BUILD_COMMAND=""
DEP_CLEAN_COMMAND="cbuild clean"
DEP_DYNAMIC_OUT_FILES=""
DEP_STATIC_OUT_FILES="bin/tim.a"
DEP_OTHER_OUT_FILES=""
PRESERVE_OUT_DIRECTORY_STRUCTURE=false

135
include/tcp-chat.h Normal file
View File

@@ -0,0 +1,135 @@
#pragma once
#include "tlibc/errors.h"
#include "tlibc/time.h"
#include "tlibc/magic.h"
/// requires tlibc and tlibtoml init
Result(void) TcpChat_init();
void TcpChat_deinit();
#define USERNAME_SIZE_MIN 2
#define USERNAME_SIZE_MAX 31
#define PASSWORD_SIZE_MIN 8
#define PASSWORD_SIZE_MAX 31
#define PASSWORD_HASH_SIZE 32
#define HOSTADDR_SIZE_MIN 4
#define HOSTADDR_SIZE_MAX 255
#define PRIVATE_KEY_BASE64_SIZE_MAX 1724
#define PUBLIC_KEY_BASE64_SIZE_MAX 699
#define SERVER_NAME_SIZE_MIN 1
#define SERVER_NAME_SIZE_MAX 127
#define SERVER_DESC_SIZE_MAX 1023
#define CHANNEL_NAME_SIZE_MIN 1
#define CHANNEL_NAME_SIZE_MAX 127
#define CHANNEL_DESC_SIZE_MAX 1023
#define MESSAGE_SIZE_MIN 1
#define MESSAGE_SIZE_MAX 4000
#define MESSAGE_BLOCK_COUNT_MAX 50
#define MESSAGE_TIMESTAMP_FMT_SQL "%Y.%m.%d-%H:%M:%f"
//////////////////////////////////////////////////////////////////////////////
// //
// Logging //
// //
//////////////////////////////////////////////////////////////////////////////
ErrorCodePage_declare(WINSOCK2);
ErrorCodePage_declare(TcpChat);
typedef enum TcpChatError {
TcpChatError_Unknown,
TcpChatError_RejectIncoming,
} TcpChatError;
typedef enum LogSeverity {
LogSeverity_Debug,
LogSeverity_Info,
LogSeverity_Warn,
LogSeverity_Error,
} LogSeverity;
typedef void (*LogFunction_t)(void* logger, cstr context, LogSeverity severity, cstr msg);
// requires defined LOGGER, LOG_FUNC, LOG_CONTEXT
#define log(severity, format, ...) { \
if(LOG_FUNC) { \
char* ___log_msg = sprintf_malloc(format ,##__VA_ARGS__); \
LOG_FUNC(LOGGER, LOG_CONTEXT, severity, ___log_msg); \
free(___log_msg); \
} \
}
#define logDebug(format, ...) log(LogSeverity_Debug, format ,##__VA_ARGS__)
#define logInfo(format, ...) log(LogSeverity_Info, format ,##__VA_ARGS__)
#define logWarn(format, ...) log(LogSeverity_Warn, format ,##__VA_ARGS__)
#define logError(format, ...) log(LogSeverity_Error, format ,##__VA_ARGS__)
//////////////////////////////////////////////////////////////////////////////
// //
// Server //
// //
//////////////////////////////////////////////////////////////////////////////
typedef struct Server Server;
/// @param config_file_content config in toml format
/// @param config_file_name to use in error messages
/// @param logger some shared data for your log function
/// @param log_func log function that you have to implement
/// @return
Result(Server*) Server_create(str config_file_content, cstr config_file_name,
void* logger, LogFunction_t log_func);
void Server_free(Server* server);
Result(void) Server_run(Server* server);
//////////////////////////////////////////////////////////////////////////////
// //
// Client //
// //
//////////////////////////////////////////////////////////////////////////////
typedef struct Client Client;
Result(Client*) Client_create(str username, str password);
void Client_free(Client* client);
/// @return username saved during client initialization
str Client_getUserName(Client* client);
/// @return AES key calculated from password that can be used to encrypt user data
Array(u8) Client_getUserDataKey(Client* client);
/// @param server_addr_cstr ip:port
/// @param server_pk_base64 public key encoded by `RSA_serializePublicKey_base64()`
Result(void) Client_connect(Client* client, cstr server_addr_cstr, cstr server_pk_base64);
/// disconnect from current server
void Client_disconnect(Client* client);
/// @param self connected client
/// @param out_str heap-allocated string
Result(void) Client_getServerName(Client* self, str* out_str);
/// @param self connected client
/// @param out_str heap-allocated string
Result(void) Client_getServerDescription(Client* self, str* out_str);
/// Create new account on connected server
Result(void) Client_register(Client* self, i64* out_user_id);
/// Authorize on connected server
Result(void) Client_login(Client* self, i64* out_user_id, i64* out_landing_channel_id);
/// @param out_timestamp timestamp received from server
/// @return message id received from server
Result(i64) Client_sendMessage(Client* self, i64 channel_id, Array(u8) content, DateTime* out_timestamp);
/// Receive a bunch of messages from the server to a client internal buffer
/// @return number of messages received
Result(u32) Client_receiveMessageBlock(Client* self, i64 channel_id, i64 first_message_id, u32 messages_count);
/// Read message saved in client internal buffer.
/// @return number of bytes written in dst_content
Result(u32) Client_popMessage(Client* self, Array(u8) dst_content, i64* message_id, i64* sender_id, DateTime* timestamp_utc);

View File

@@ -1,34 +0,0 @@
#pragma once
#include "tlibc/errors.h"
#include "tlibc/string/str.h"
typedef struct Client Client;
Result(Client*) Client_create(str username, str password);
void Client_free(Client* client);
/// @return username saved during client initialization
str Client_getUserName(Client* client);
/// @return AES key calculated from password that can be used to encrypt user data
Array(u8) Client_getUserDataKey(Client* client);
/// @param server_addr_cstr ip:port
/// @param server_pk_base64 public key encoded by `RSA_serializePublicKey_base64()`
Result(void) Client_connect(Client* client, cstr server_addr_cstr, cstr server_pk_base64);
/// disconnect from current server
void Client_disconnect(Client* client);
/// @param self connected client
/// @param out_name owned by Client, fetched from server during Client_connect
Result(void) Client_getServerName(Client* self, str* out_name);
/// @param self connected client
/// @param out_name owned by Client, fetched from server during Client_connect
Result(void) Client_getServerDescription(Client* self, str* out_desc);
/// Create new account on connected server
Result(void) Client_register(Client* self, u64* out_user_id);
/// Authorize on connected server
Result(void) Client_login(Client* self, u64* out_user_id, u64* out_landing_channel_id);

View File

@@ -1,21 +0,0 @@
#pragma once
#include "tlibc/std.h"
#define USERNAME_SIZE_MIN 2
#define USERNAME_SIZE_MAX 31
#define PASSWORD_SIZE_MIN 8
#define PASSWORD_SIZE_MAX 31
#define PASSWORD_HASH_SIZE 32
#define HOSTADDR_SIZE_MIN 4
#define HOSTADDR_SIZE_MAX 255
#define PRIVATE_KEY_BASE64_SIZE_MAX 1724
#define PUBLIC_KEY_BASE64_SIZE_MAX 699
#define SERVER_NAME_SIZE_MIN 1
#define SERVER_NAME_SIZE_MAX 127
#define SERVER_DESC_SIZE_MAX 1023
#define CHANNEL_NAME_SIZE_MIN 1
#define CHANNEL_NAME_SIZE_MAX 127
#define CHANNEL_DESC_SIZE_MAX 1023
#define MESSAGE_SIZE_MIN 1
#define MESSAGE_SIZE_MAX 4000
#define MESSAGE_BLOCK_SIZE (64*1024)

View File

@@ -1,28 +0,0 @@
#pragma once
#include <stdio.h>
#include "tlibc/std.h"
#include "tlibc/string/cstr.h"
typedef enum LogSeverity {
LogSeverity_Debug,
LogSeverity_Info,
LogSeverity_Warn,
LogSeverity_Error,
} LogSeverity;
typedef void (*LogFunction_t)(void* logger, cstr context, LogSeverity severity, cstr msg);
// requires defined LOGGER, LOG_FUNC, LOG_CONTEXT
#define log(severity, format, ...) { \
if(LOG_FUNC) { \
char* ___log_msg = sprintf_malloc(format ,##__VA_ARGS__); \
LOG_FUNC(LOGGER, LOG_CONTEXT, severity, ___log_msg); \
free(___log_msg); \
} \
}
#define logDebug(format, ...) log(LogSeverity_Debug, format ,##__VA_ARGS__)
#define logInfo(format, ...) log(LogSeverity_Info, format ,##__VA_ARGS__)
#define logWarn(format, ...) log(LogSeverity_Warn, format ,##__VA_ARGS__)
#define logError(format, ...) log(LogSeverity_Error, format ,##__VA_ARGS__)

View File

@@ -1,18 +0,0 @@
#pragma once
#include "tlibc/errors.h"
#include "tlibc/string/str.h"
#include "tcp-chat/log.h"
typedef struct Server Server;
/// @param config_file_content config in toml format
/// @param config_file_name to use in error messages
/// @param logger some shared data for your log function
/// @param log_func log function that you have to implement
/// @return
Result(Server*) Server_create(str config_file_content, cstr config_file_name,
void* logger, LogFunction_t log_func);
void Server_free(Server* server);
Result(void) Server_run(Server* server);

View File

@@ -1,14 +0,0 @@
#pragma once
#include "tlibc/errors.h"
/// requires tlibc and tlibtoml init
Result(void) TcpChat_init();
void TcpChat_deinit();
ErrorCodePage_declare(WINSOCK2);
ErrorCodePage_declare(TcpChat);
typedef enum TcpChatError {
TcpChatError_Unknown,
TcpChatError_RejectIncoming,
} TcpChatError;

View File

@@ -24,7 +24,7 @@ SRC_CPP="$(find src -name '*.cpp')"
# See cbuild/example_dependency_configs
DEPENDENCY_CONFIGS_DIR='dependencies'
# List of dependency config files in DEPENDENCY_CONFIGS_DIR separated by space.
ENABLED_DEPENDENCIES='bearssl tlibc tlibtoml tsqlite'
ENABLED_DEPENDENCIES='bearssl tlibc tlibtoml tsqlite tim'
# OBJDIR structure:
# ├── objects/ - Compiled object files. Cleans on each call of build task
@@ -38,7 +38,9 @@ STATIC_LIB_FILE="$PROJECT.a"
INCLUDE="-Isrc -Iinclude
-I$DEPENDENCIES_DIR/BearSSL/inc
-I$DEPENDENCIES_DIR/tlibc/include
-I$DEPENDENCIES_DIR/tlibtoml/include"
-I$DEPENDENCIES_DIR/tlibtoml/include
-I$DEPENDENCIES_DIR/tsqlite/include
-I$DEPENDENCIES_DIR/tim/include"
# OS-specific options
case "$OS" in
@@ -46,7 +48,7 @@ case "$OS" in
EXEC_FILE="$PROJECT.exe"
SHARED_LIB_FILE="$PROJECT.dll"
INCLUDE="$INCLUDE "
LINKER_LIBS="-static -lpthread -lws2_32 -lsqlite3"
LINKER_LIBS="-static -lpthread -lws2_32 -luuid -lsqlite3"
;;
LINUX)
EXEC_FILE="$PROJECT"

View File

@@ -1,438 +1,66 @@
#include "ClientCLI.h"
#include "tlibc/filesystem.h"
#include "tlibc/term.h"
#include "tcp-chat/common_constants.h"
#include "cli/ClientCLI/ClientCLI.h"
#include "network/tcp-chat-protocol/v1.h"
static const str greeting_art = STR(
" ^,,^ |\n"
" ( •·•) Meum! (o.o`7\n"
" / ` | Meum... |`˜ \\\n"
"\\(_,J J L l`,)/\n"
);
static const str farewell_art = STR(
" ^,,^ |\n"
" ( -.-) (>,<`7\n"
" / ` | Goodbye! |`˜ \\\n"
"\\(_,J J L l`,)/\n"
);
#define is_alias(LITERAL) str_equals(command, STR(LITERAL))
static Result(void) ClientCLI_askUserNameAndPassword(str* username_out, str* password_out);
static Result(void) ClientCLI_execCommand(ClientCLI* self, str command, bool* stop);
static Result(void) ClientCLI_openUserDB(ClientCLI* self);
static Result(ServerInfo*) ClientCLI_saveServerInfo(ClientCLI* self,
str addr, str pk_base64, str name, str desc);
static Result(ServerInfo*) ClientCLI_joinNewServer(ClientCLI* self);
static Result(ServerInfo*) ClientCLI_selectServerFromCache(ClientCLI* self);
static Result(void) ClientCLI_showServerInfo(ClientCLI* self, ServerInfo* server);
static Result(void) ClientCLI_register(ClientCLI* self);
static Result(void) ClientCLI_login(ClientCLI* self);
#include <assert.h>
void ClientCLI_destroy(ClientCLI* self){
if(!self)
return;
Client_free(self->client);
idb_close(self->db);
List_ServerInfo_destroy(&self->servers.list);
HashMap_destroy(&self->servers.addr_id_map);
ClientQueries_free(self->queries);
tsqlite_connection_close(self->db);
List_SavedServer_destroyWithElements(&self->saved_servers, SavedServer_destroy);
}
void ClientCLI_construct(ClientCLI* self){
zeroStruct(self);
self->style.common = (TimStyle){
.brd = Color256_LightGray,
.bg = Color256_NavyBlue,
.fg = Color256_LightGray
};
self->style.focused = (TimStyle){
.brd = Color256_White,
.bg = Color256_DeepSkyBlue,
.fg = Color256_White
};
self->style.error = (TimStyle){
.brd = Color256_LightGray,
.bg = Color256_DarkRed,
.fg = Color256_White
};
self->saved_servers = List_SavedServer_alloc(0);
}
Result(void) ClientCLI_run(ClientCLI* self) {
Deferral(16);
void ClientCLI_run(ClientCLI* self) {
Deferral(32);
try_void(term_init());
term_clear();
printf(FMT_str"\n", greeting_art.len, greeting_art.data);
StartScreenContext start_screen_ctx;
StartScreenContext_construct(&start_screen_ctx, self);
Defer(StartScreenContext_destroy(&start_screen_ctx));
MainScreenContext main_screen_ctx;
MainScreenContext_construct(&main_screen_ctx, self);
Defer(MainScreenContext_destroy(&main_screen_ctx));
// create Client
str username = str_null, password = str_null;
try_void(ClientCLI_askUserNameAndPassword(&username, &password));
Defer(
str_destroy(username);
str_destroy(password);
);
Client_free(self->client);
try(self->client, p, Client_create(username, password));
memset(password.data, 0, password.len);
// init db
try_void(ClientCLI_openUserDB(self));
char input_buf[1024];
str command_input = str_null;
bool stop = false;
while(!stop){
sleepMsec(50);
printf("> ");
try_void(term_readLine(input_buf, sizeof(input_buf)));
command_input = str_from_cstr(input_buf);
str_trim(&command_input, true);
if(command_input.len == 0)
continue;
ResultVar(void) com_result = ClientCLI_execCommand(self, command_input, &stop);
if(com_result.error){
Error_addCallPos(com_result.error, ErrorCallPos_here());
str e_str = Error_toStr(com_result.error);
printf("\n"FMT_str"\n", e_str.len, e_str.data);
str_destroy(e_str);
Error_free(com_result.error);
while(tim_run(FPS)){
switch(self->state){
case ClientCLIState_Exit:
Return;
default:
assert(false && "invalid ClientCLI state");
break;
case ClientCLIState_StartScreen:
start_screen(&start_screen_ctx);
break;
case ClientCLIState_MainScreen:
main_screen(&main_screen_ctx);
break;
}
}
Return RESULT_VOID;
}
static Result(void) ClientCLI_askUserNameAndPassword(str* username_out, str* password_out){
Deferral(8);
bool success = false;
// ask username
Array(char) username_buf = Array_char_alloc(128);
Defer(if(!success) Array_char_destroy(&username_buf));
str username = str_null;
while(true) {
printf("username: ");
try_void(term_readLine(username_buf.data, username_buf.len));
username = str_from_cstr(username_buf.data);
str_trim(&username, true);
str name_error_str = validateUsername_str(username);
if(name_error_str.data){
printf("ERROR: "FMT_str"\n",
name_error_str.len, name_error_str.data);
}
else break;
}
// ask password
Array(char) password_buf = Array_char_alloc(128);
Defer(if(!success) Array_char_destroy(&password_buf));
str password = str_null;
while(true) {
printf("password: ");
// TODO: hide password
try_void(term_readLineHidden(password_buf.data, password_buf.len));
password = str_from_cstr(password_buf.data);
str_trim(&password, true);
if(password.len < PASSWORD_SIZE_MIN || password.len > PASSWORD_SIZE_MAX){
printf("ERROR: password length (in bytes) must be >= %i and <= %i\n",
PASSWORD_SIZE_MIN, PASSWORD_SIZE_MAX);
}
else break;
}
*username_out = username;
*password_out = password;
success = true;
Return RESULT_VOID;
}
static Result(void) ClientCLI_execCommand(ClientCLI* self, str command, bool* stop){
Deferral(64);
if(is_alias("q") || is_alias("quit") || is_alias("exit")){
printf(FMT_str"\n", farewell_art.len, farewell_art.data);
*stop = true;
}
else if(is_alias("clear")){
term_clear();
}
else if(is_alias("h") || is_alias("help")){
printf(
"COMMANDS:\n"
"Without connection:\n"
" h, help Show this message.\n"
" q, quit, exit Close the program.\n"
" clear Clear the screen.\n"
"Connection:\n"
" j, join Join a new server and select it.\n"
" s, select Select a server you joined before.\n"
"After connection:\n"
" r, register Create account on selected server\n"
" l, login Authorize on selected server\n"
"Authorized:\n"
);
}
else if (is_alias("j") || is_alias("join")){
// ask address and key, connect to server
try_void(ClientCLI_joinNewServer(self));
}
else if(is_alias("s") || is_alias("select")){
// show scrollable list of servers, get selected one
try_void(ClientCLI_selectServerFromCache(self));
}
else if(is_alias("r") || is_alias("register")){
try_void(ClientCLI_register(self));
}
else if(is_alias("l") || is_alias("login")){
try_void(ClientCLI_login(self));
// TODO: call Client_runIO():
// function with infinite loop which sends and receives messages
// with navigation across server channels
}
else {
printf("ERROR: unknown command.\n"
"Use 'h' to see list of avaliable commands\n");
}
Return RESULT_VOID;
}
static Result(void) ClientCLI_joinNewServer(ClientCLI* self){
Deferral(8);
// ask server address
printf("Enter server address (ip:port):\n");
char server_addr_cstr[HOSTADDR_SIZE_MAX + 1];
try_void(term_readLine(server_addr_cstr, sizeof(server_addr_cstr)));
str server_addr_str = str_from_cstr(server_addr_cstr);
str_trim(&server_addr_str, true);
// ask server public key
printf("Enter server public key (RSA-Public-<SIZE>:<DATA>):\n");
char server_pk_cstr[PUBLIC_KEY_BASE64_SIZE_MAX + 1];
try_void(term_readLine(server_pk_cstr, sizeof(server_pk_cstr)));
str server_pk_str = str_from_cstr(server_pk_cstr);
str_trim(&server_pk_str, true);
printf("Connecting to server...\n");
try_void(Client_connect(self->client, server_addr_cstr, server_pk_cstr));
printf("Connection established\n");
str server_name = str_null;
str server_description = str_null;
try_void(Client_getServerName(self->client, &server_name));
try_void(Client_getServerDescription(self->client, &server_description));
try(ServerInfo* server, p, ClientCLI_saveServerInfo(self,
server_addr_str, server_pk_str,
server_name, server_description));
try_void(ClientCLI_showServerInfo(self, server));
Return RESULT_VOID;
}
static Result(void) ClientCLI_selectServerFromCache(ClientCLI* self){
Deferral(8);
// Lock table until this function returns.
// It may not change any data in table, but it uses associated cache structures.
idb_lockTable(self->servers.table);
Defer(idb_unlockTable(self->servers.table));
u32 servers_count = self->servers.list.len;
if(servers_count == 0){
printf("No servers found in cache\n");
Return RESULT_VOID;
}
for(u32 id = 0; id < servers_count; id++){
ServerInfo* server = self->servers.list.data + id;
printf("[%02u] "FMT_str" "FMT_str"\n",
id, server->address_len, server->address, server->name_len, server->name);
}
char buf[32];
u32 id = -1;
while(true) {
printf("Type 'q' to cancel\n");
printf("Select server (number): ");
try_void(term_readLine(buf, sizeof(buf)));
str input_line = str_from_cstr(buf);
str_trim(&input_line, true);
if(str_equals(input_line, STR("q"))){
Return RESULT_VOID;
}
if(sscanf(buf, FMT_u32, &id) != 1){
printf("ERROR: not a number\n");
}
else if(id >= servers_count){
printf("ERROR: not a server number: %u\n", id);
}
else break;
}
ServerInfo* server = self->servers.list.data + id;
printf("Connecting to '"FMT_str"'...\n", server->address_len, server->address);
try_void(Client_connect(self->client, server->address, server->pk_base64));
printf("Connection established\n");
bool server_info_changed = false;
// update cached server name
str name = str_null;
try_void(Client_getServerName(self->client, &name));
if(!str_equals(name, str_construct(server->name, server->name_len, true))){
server_info_changed = true;
if(name.len > SERVER_NAME_SIZE_MAX)
name.len = SERVER_NAME_SIZE_MAX;
server->name_len = name.len;
memcpy(server->name, name.data, server->name_len);
}
// update cached server description
str desc = str_null;
try_void(Client_getServerDescription(self->client, &desc));
if(!str_equals(desc, str_construct(server->desc, server->desc_len, true))){
server_info_changed = true;
if(desc.len > SERVER_DESC_SIZE_MAX)
desc.len = SERVER_DESC_SIZE_MAX;
server->desc_len = desc.len;
memcpy(server->desc, desc.data, server->desc_len);
}
if(server_info_changed){
try_void(idb_updateRow(self->servers.table, id, server, false));
}
try_void(ClientCLI_showServerInfo(self, server));
Return RESULT_VOID;
}
static Result(void) ClientCLI_showServerInfo(ClientCLI* self, ServerInfo* server){
Deferral(8);
(void)self;
printf("Server Name: "FMT_str"\n", server->name_len, server->name);
printf("Host Address: "FMT_str"\n", server->address_len, server->address);
printf("Description:\n"FMT_str"\n\n", server->desc_len, server->desc);
printf("Public Key:\n" FMT_str"\n\n", server->pk_base64_len, server->pk_base64);
printf("Type 'register' if you don't have an account on the server.\n");
printf("Type 'login' to authorize on the server.\n");
Return RESULT_VOID;
}
static Result(void) ClientCLI_openUserDB(ClientCLI* self){
Deferral(8);
str username = Client_getUserName(self->client);
Array(u8) user_data_key = Client_getUserDataKey(self->client);
str user_db_dir = str_from_cstr(strcat_malloc("client-db", path_seps, username.data));
Defer(free(user_db_dir.data));
try(self->db, p, idb_open(user_db_dir, user_data_key));
// Lock DB until this function returns.
idb_lockDB(self->db);
Defer(idb_unlockDB(self->db));
// Load servers table
try(self->servers.table, p,
idb_getOrCreateTable(self->db, str_null, STR("servers"), sizeof(ServerInfo), false)
);
// Lock table until this function returns.
idb_lockTable(self->servers.table);
Defer(idb_unlockTable(self->servers.table));
// load whole servers table to list
try_void(
idb_createListFromTable(self->servers.table, (void*)&self->servers.list, false)
);
// build address-id map
try(u64 servers_count, u,
idb_getRowCount(self->servers.table, false)
);
HashMap_construct(&self->servers.addr_id_map, u64, NULL);
for(u64 id = 0; id < servers_count; id++){
ServerInfo* server = self->servers.list.data + id;
str key = str_construct(server->address, server->address_len, true);
if(!HashMap_tryPush(&self->servers.addr_id_map, key, &id)){
Return RESULT_ERROR_FMT(
"duplicate server address '"FMT_str"'",
key.len, key.data);
}
}
Return RESULT_VOID;
}
static Result(ServerInfo*) ClientCLI_saveServerInfo(ClientCLI* self,
str addr, str pk_base64, str name, str desc){
Deferral(8);
// create new server info
ServerInfo server;
zeroStruct(&server);
// address
if(addr.len > HOSTADDR_SIZE_MAX)
addr.len = HOSTADDR_SIZE_MAX;
server.address_len = addr.len;
memcpy(server.address, addr.data, server.address_len);
// public key
if(pk_base64.len > PUBLIC_KEY_BASE64_SIZE_MAX)
pk_base64.len = PUBLIC_KEY_BASE64_SIZE_MAX;
server.pk_base64_len = pk_base64.len;
memcpy(server.pk_base64, pk_base64.data, server.pk_base64_len);
// name
if(name.len > SERVER_NAME_SIZE_MAX)
name.len = SERVER_NAME_SIZE_MAX;
server.name_len = name.len;
memcpy(server.name, name.data, server.name_len);
// description
if(desc.len > SERVER_DESC_SIZE_MAX)
desc.len = SERVER_DESC_SIZE_MAX;
server.desc_len = desc.len;
memcpy(server.desc, desc.data, server.desc_len);
// Lock table until this function returns.
// It may not change any data in table, but it uses associated cache structures.
idb_lockTable(self->servers.table);
Defer(idb_unlockTable(self->servers.table));
// try find server id in cache
ServerInfo* cached_row_ptr = NULL;
u64* id_ptr = NULL;
id_ptr = HashMap_tryGetPtr(&self->servers.addr_id_map, addr);
if(id_ptr){
// update existing server
u64 id = *id_ptr;
try_void(idb_updateRow(self->servers.table, id, &server, false));
try_assert(id < self->servers.list.len);
cached_row_ptr = self->servers.list.data + id;
memcpy(cached_row_ptr, &server, sizeof(ServerInfo));
}
else {
// push new server
try(u64 id, u, idb_pushRow(self->servers.table, &server, false));
try_assert(id == self->servers.list.len);
List_ServerInfo_pushMany(&self->servers.list, &server, 1);
cached_row_ptr = self->servers.list.data + id;
try_assert(HashMap_tryPush(&self->servers.addr_id_map, addr, &id));
}
Return RESULT_VALUE(p, cached_row_ptr);
}
static Result(void) ClientCLI_register(ClientCLI* self){
Deferral(8);
u64 user_id = 0;
try_void(Client_register(self->client, &user_id));
printf("Registered successfully\n");
printf("user_id: "FMT_u64"\n", user_id);
// TODO: use user_id somewhere
Return RESULT_VOID;
}
static Result(void) ClientCLI_login(ClientCLI* self){
Deferral(8);
u64 user_id = 0, landing_channel_id = 0;
try_void(Client_login(self->client, &user_id, &landing_channel_id));
printf("Authorized successfully\n");
printf("user_id: "FMT_u64", landing_channel_id: "FMT_u64"\n", user_id, landing_channel_id);
// TODO: use user_id, landing_channel_id somewhere
Return RESULT_VOID;
Return;
}

View File

@@ -1,23 +1,95 @@
#pragma once
#include <pthread.h>
#include "tcp-chat.h"
#include "tlibc/collections/HashMap.h"
#include "tlibc/collections/List.h"
#include "tcp-chat/client.h"
#include "db/idb.h"
#include "db/tables.h"
#include "db/client_db.h"
#include "tim.h"
List_declare(ServerInfo);
#define FPS 30
typedef enum ClientCLIState {
ClientCLIState_StartScreen,
ClientCLIState_Exit,
ClientCLIState_MainScreen,
ClientCLIState_ServerChannels,
ClientCLIState_ChannelChat,
} ClientCLIState;
typedef struct ClientCLI {
Client* client;
IncrementalDB* db;
ClientCLIState state;
struct {
Table* table;
List(ServerInfo) list; // index is id
HashMap(u64) addr_id_map; // key is server address
} servers;
TimStyle common;
TimStyle focused;
TimStyle error;
} style;
Client* client;
tsqlite_connection* db;
ClientQueries* queries;
List(SavedServer) saved_servers;
} ClientCLI;
void ClientCLI_construct(ClientCLI* self);
void ClientCLI_destroy(ClientCLI* self);
Result(void) ClientCLI_run(ClientCLI* self);
void ClientCLI_run(ClientCLI* self);
enum {
Color256_Black = 0x10,
Color256_MidGray = 0xf8,
Color256_LightGray = 0xfb,
Color256_White = 0xe7,
Color256_NavyBlue = 0x11,
Color256_DeepSkyBlue = 0x18,
Color256_DarkRed = 0x58,
};
typedef struct TextInputState {
TimEditState edit;
cstr label;
struct {
TimStyle common;
TimStyle focused;
} style;
TimKey result_key;
} TextInputState;
void TextInputState_construct(TextInputState* ctx, cstr label,
Array(char) buf, NULLABLE(cstr) initial_value, bool masked,
TimStyle common, TimStyle focused);
/// tim_edit with tim_label over its upper border.
void text_input(TextInputState* ctx, i32 x, i32 y, i32 w, TimStyle style);
void TimPanelItem_fromTextInputState(TimPanelItem* item, TextInputState* input);
/// Intended to use in TimPanelItem
/// @param data TextInputState*
void draw_item_text_input(void* data, TimRect place, bool is_selected);
List_declare(TimPanelItem);
typedef struct StartScreenContext {
ClientCLI* client;
char* err_msg; // heap only
TextInputState input_username;
TextInputState input_password;
TimPanel central_panel;
TimPanel central_buttons_panel;
} StartScreenContext;
void StartScreenContext_construct(StartScreenContext* ctx, ClientCLI* client);
void StartScreenContext_destroy(StartScreenContext* ctx);
void start_screen(StartScreenContext* ctx);
typedef struct MainScreenContext {
ClientCLI* client;
TimPanel central_panel;
TimScrollView central_scroll_view;
} MainScreenContext;
void MainScreenContext_construct(MainScreenContext* ctx, ClientCLI* client);
void MainScreenContext_destroy(MainScreenContext* ctx);
void main_screen(MainScreenContext* ctx);

View File

@@ -0,0 +1,98 @@
#include "client_db_internal.h"
void SavedServer_destroy(SavedServer* self){
if(!self)
return;
str_destroy(self->address);
str_destroy(self->pk_base64);
str_destroy(self->name);
str_destroy(self->description);
}
Result(bool) SavedServer_exists(ClientQueries* q, str address){
Deferral(4);
tsqlite_statement* st = q->servers.exists;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_str(st, "$address", address, NULL));
try(bool has_result, i, tsqlite_statement_step(st));
Return RESULT_VALUE(i, has_result);
}
Result(bool) SavedServer_comparePublicKey(ClientQueries* q, str address, str pk_base64){
Deferral(4);
tsqlite_statement* st = q->servers.compare_pk;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_str(st, "$address", address, NULL));
try_void(tsqlite_statement_bind_str(st, "$pk_base64", pk_base64, NULL));
try(bool has_result, i, tsqlite_statement_step(st));
Return RESULT_VALUE(i, has_result);
}
Result(void) SavedServer_createOrUpdate(ClientQueries* q, SavedServer* server){
Deferral(4);
try_assert(server->address.len >= HOSTADDR_SIZE_MIN && server->address.len <= HOSTADDR_SIZE_MAX);
try_assert(server->pk_base64.len > 0 && server->pk_base64.len <= PUBLIC_KEY_BASE64_SIZE_MAX);
try_assert(server->name.len >= SERVER_NAME_SIZE_MIN && server->name.len <= SERVER_NAME_SIZE_MAX);
try_assert(server->description.len <= SERVER_DESC_SIZE_MAX);
try(bool server_exists, i, SavedServer_exists(q, server->address));
tsqlite_statement* st = NULL;
Defer(tsqlite_statement_reset(st));
if(server_exists){
st = q->servers.update;
try(bool pk_matches, i, SavedServer_comparePublicKey(q, server->address, server->pk_base64));
if(!pk_matches){
Return RESULT_ERROR_FMT(
"trying to update server '"FMT_str"' but public keys don't match",
str_unwrap(server->address));
}
}
else {
st = q->servers.insert;
try_void(tsqlite_statement_bind_str(st, "$pk_base64", server->pk_base64, NULL));
}
try_void(tsqlite_statement_bind_str(st, "$address", server->address, NULL));
try_void(tsqlite_statement_bind_str(st, "$name", server->name, NULL));
try_void(tsqlite_statement_bind_str(st, "$description", server->description, NULL));
try_void(tsqlite_statement_step(st));
Return RESULT_VALUE(i, !server_exists);
}
Result(void) SavedServer_getAll(ClientQueries* q, List(SavedServer)* dst_list){
Deferral(4);
tsqlite_statement* st = q->servers.get_all;
Defer(tsqlite_statement_reset(st));
SavedServer server = SavedServer_construct(str_null, str_null, str_null, str_null);
str tmp_str = str_null;
while(true){
try(bool has_result, i, tsqlite_statement_step(st));
if(!has_result)
break;
// address
try_void(tsqlite_statement_getResult_str(st, &tmp_str));
server.address = str_copy(tmp_str);
// pk_base64
try_void(tsqlite_statement_getResult_str(st, &tmp_str));
server.pk_base64 = str_copy(tmp_str);
// name
try_void(tsqlite_statement_getResult_str(st, &tmp_str));
server.name = str_copy(tmp_str);
// description
try_void(tsqlite_statement_getResult_str(st, &tmp_str));
server.description = str_copy(tmp_str);
List_SavedServer_pushMany(dst_list, &server, 1);
}
Return RESULT_VOID;
}

View File

@@ -0,0 +1,81 @@
#include "client_db_internal.h"
#include "tlibc/filesystem.h"
Result(tsqlite_connection* db) ClientDatabase_open(cstr file_path){
Deferral(64);
try_void(dir_createParent(file_path));
try(tsqlite_connection* db, p, tsqlite_connection_open(file_path));
bool success = false;
Defer(if(!success) tsqlite_connection_close(db));
///////////////////////////////////////////////////////////////////////////
// SERVERS //
///////////////////////////////////////////////////////////////////////////
try(tsqlite_statement* create_table_servers, p, tsqlite_statement_compile(db, STR(
"CREATE TABLE IF NOT EXISTS servers (\n"
" address VARCHAR PRIMARY KEY,\n"
" pk_base64 VARCHAR NOT NULL,\n"
" name VARCHAR NOT NULL,\n"
" description VARCHAR NOT NULL\n"
");"
)));
Defer(tsqlite_statement_free(create_table_servers));
try_void(tsqlite_statement_step(create_table_servers));
success = true;
Return RESULT_VALUE(p, db);
}
void ClientQueries_free(ClientQueries* q){
if(!q)
return;
tsqlite_statement_free(q->servers.insert);
tsqlite_statement_free(q->servers.update);
tsqlite_statement_free(q->servers.exists);
tsqlite_statement_free(q->servers.compare_pk);
tsqlite_statement_free(q->servers.get_all);
free(q);
}
Result(ClientQueries*) ClientQueries_compile(tsqlite_connection* db){
Deferral(4);
ClientQueries* q = (ClientQueries*)malloc(sizeof(*q));
zeroStruct(q);
bool success = false;
Defer(if(!success) ClientQueries_free(q));
///////////////////////////////////////////////////////////////////////////
// SERVERS //
///////////////////////////////////////////////////////////////////////////
try(q->servers.insert, p, tsqlite_statement_compile(db, STR(
"INSERT INTO\n"
"servers (address, pk_base64, name, description)\n"
"VALUES ($address, $pk_base64, $name, $description);"
)));
try(q->servers.update, p, tsqlite_statement_compile(db, STR(
"UPDATE servers\n"
"SET name = $name, description = $description\n"
"WHERE address = $address;"
)));
try(q->servers.exists, p, tsqlite_statement_compile(db, STR(
"SELECT 1 FROM servers WHERE address = $address;"
)));
try(q->servers.compare_pk, p, tsqlite_statement_compile(db, STR(
"SELECT 1 FROM servers WHERE address = $address AND pk_base64 = $pk_base64;"
)));
try(q->servers.get_all, p, tsqlite_statement_compile(db, STR(
"SELECT * FROM servers;"
)));
success = true;
Return RESULT_VALUE(p, q);
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "tcp-chat.h"
#include "tsqlite.h"
#include "network/tcp-chat-protocol/v1.h"
#include "tlibc/collections/List.h"
/// @brief open DB and create tables
Result(tsqlite_connection* db) ClientDatabase_open(cstr file_path);
typedef struct ClientQueries ClientQueries;
Result(ClientQueries*) ClientQueries_compile(tsqlite_connection* db);
void ClientQueries_free(ClientQueries* self);
typedef struct SavedServer {
str address;
str pk_base64;
str name;
str description;
} SavedServer;
List_declare(SavedServer);
#define SavedServer_construct(ADDR, PK, NAME, DESC) ((SavedServer){ \
.address = ADDR, .pk_base64 = PK, .name = NAME, .description = DESC })
void SavedServer_destroy(SavedServer* self);
/// @return true if new row was created
Result(bool) SavedServer_createOrUpdate(ClientQueries* q, SavedServer* server);
/// @param dst_list there SavedServer values are pushed
Result(void) SavedServer_getAll(ClientQueries* q, List(SavedServer)* dst_list);
Result(bool) SavedServer_exists(ClientQueries* q, str address);
/// @return true if provided key and saved key match
Result(bool) SavedServer_comparePublicKey(ClientQueries* q, str address, str pk_base64);

View File

@@ -0,0 +1,17 @@
#pragma once
#include "client_db.h"
typedef struct ClientQueries {
struct {
/* ($address, $pk_base64, $name, $description) -> void */
tsqlite_statement* insert;
/* ($address, $name, $description) -> void */
tsqlite_statement* update;
/* ($address) -> 1 or nothing */
tsqlite_statement* exists;
/* ($address, $pk_base64) -> 1 or nothing */
tsqlite_statement* compare_pk;
/* () -> [(*)] */
tsqlite_statement* get_all;
} servers;
} ClientQueries;

View File

@@ -0,0 +1,302 @@
#include "ClientCLI.h"
#include "tlibc/term.h"
#include "network/tcp-chat-protocol/v1.h"
#include "tim.h"
static const str greeting_art = STR(
" ^,,^ |\n"
" ( •·•) Meum! (o.o`7\n"
" / ` | Meum... |`˜ \\\n"
"\\(_,J J L l`,)/\n"
);
static const str farewell_art = STR(
" ^,,^ |\n"
" ( -.-) (>,<`7\n"
" / ` | Goodbye! |`˜ \\\n"
"\\(_,J J L l`,)/\n"
);
static void draw_test_label(void* data, TimRect place, bool is_selected);
static void draw_central_panel(void* data, TimRect place);
static Result(SavedServer*) joinNewServer(ClientCLI* self);
static Result(SavedServer*) selectServerFromCache(ClientCLI* self);
static Result(void) showSavedServer(ClientCLI* self, SavedServer* server);
static Result(void) registerAtServer(ClientCLI* self);
static Result(void) loginAtServer(ClientCLI* self);
void MainScreenContext_construct(MainScreenContext* ctx, ClientCLI* client){
zeroStruct(ctx);
ctx->client = client;
///////////////////////////////////////////////////////////////////////////
// central_panel //
///////////////////////////////////////////////////////////////////////////
{
List(TimPanelItem) items = List_TimPanelItem_alloc(4);
TimPanelItem item_test_label = {
.w = A, .h = 12, .data = ctx, .draw = draw_test_label
};
List_TimPanelItem_push(&items, item_test_label);
List_TimPanelItem_push(&items, item_test_label);
List_TimPanelItem_push(&items, item_test_label);
List_TimPanelItem_push(&items, item_test_label);
ctx->central_panel.items = items.data;
ctx->central_panel.len = items.len;
}
///////////////////////////////////////////////////////////////////////////
// central_scroll_view //
///////////////////////////////////////////////////////////////////////////
{
ctx->central_scroll_view.content_h = 50;
ctx->central_scroll_view.data = ctx;
ctx->central_scroll_view.draw = draw_central_panel;
}
}
void MainScreenContext_destroy(MainScreenContext* ctx){
free(ctx->central_panel.items);
}
void main_screen(MainScreenContext* ctx){
if (tim->event.type == TimEvent_Draw) {
tim_fill(tim_cell(" ", ctx->client->style.common.fg, ctx->client->style.common.bg), 0, 0, A, A);
}
if(tim_button_noborder("[Esc/Q] Exit", 1, 0, 14, 1, ctx->client->style.common)
|| tim_is_key_press('q')
|| tim_is_key_press(TimKey_Escape))
{
ctx->client->state = ClientCLIState_Exit;
}
tim_scroll_view(&ctx->central_scroll_view, 0, 1, ~0, ~0, ctx->client->style.common);
}
static void draw_test_label(void* data, TimRect place, bool is_selected){
MainScreenContext* ctx = data;
TimStyle style = is_selected ? ctx->client->style.focused : ctx->client->style.common;
tim_frame(place.x, place.y, place.w, place.h, style);
tim_label("0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
place.x + 1, place.y + 1, place.w - 2, place.h - 2, style);
}
static void draw_central_panel(void* data, TimRect place){
MainScreenContext* ctx = data;
tim_panel(&ctx->central_panel, false, place.x, place.y, place.w, place.h);
}
/*
static Result(void) ClientCLI_execCommand(ClientCLI* self, str command, bool* stop){
Deferral(64);
if(is_alias("q") || is_alias("quit") || is_alias("exit")){
printf(FMT_str"\n", farewell_art.len, farewell_art.data);
*stop = true;
}
else if(is_alias("clear")){
term_clear();
}
else if(is_alias("h") || is_alias("help")){
printf(
"COMMANDS:\n"
"Without connection:\n"
" h, help Show this message.\n"
" q, quit, exit Close the program.\n"
" clear Clear the screen.\n"
"Connection:\n"
" j, join Join a new server and select it.\n"
" s, select Select a server you joined before.\n"
"After connection:\n"
" r, register Create account on selected server\n"
" l, login Authorize on selected server\n"
"Authorized:\n"
);
}
else if (is_alias("j") || is_alias("join")){
// ask address and key, connect to server
try_void(joinNewServer(self));
}
else if(is_alias("s") || is_alias("select")){
// show scrollable list of servers, get selected one
try_void(selectServerFromCache(self));
}
else if(is_alias("r") || is_alias("register")){
try_void(registerAtServer(self));
}
else if(is_alias("l") || is_alias("login")){
try_void(loginAtServer(self));
// TODO: call Client_runIO():
// function with infinite loop which sends and receives messages
// with navigation across server channels
}
else {
printf("ERROR: unknown command.\n"
"Use 'h' to see list of avaliable commands\n");
}
Return RESULT_VOID;
}
*/
static Result(void) joinNewServer(ClientCLI* self){
Deferral(8);
bool success = false;
// ask server address
const u32 address_alloc_size = HOSTADDR_SIZE_MAX + 1;
str address = str_construct((char*)malloc(address_alloc_size), address_alloc_size, true);
Defer(if(!success) str_destroy(address));
printf("Enter server address (ip:port):\n");
try_void(term_readLine(address.data, address.len));
address.len = strlen(address.data);
str_trim(&address, true);
// ask server public key
const u32 server_pk_alloc_size = PUBLIC_KEY_BASE64_SIZE_MAX + 1;
str server_pk = str_construct((char*)malloc(server_pk_alloc_size), server_pk_alloc_size, true);
Defer(if(!success) str_destroy(server_pk));
printf("Enter server public key (RSA-Public-<SIZE>:<DATA>):\n");
try_void(term_readLine(server_pk.data, server_pk.len));
server_pk.len = strlen(server_pk.data);
str_trim(&server_pk, true);
printf("Connecting to server...\n");
try_void(Client_connect(self->client, address.data, server_pk.data));
printf("Connection established\n");
str server_name = str_null;
try_void(Client_getServerName(self->client, &server_name));
Defer(if(!success) str_destroy(server_name));
str server_description = str_null;
try_void(Client_getServerDescription(self->client, &server_description));
Defer(if(!success) str_destroy(server_description));
SavedServer server = SavedServer_construct(
address,
server_pk,
server_name,
server_description
);
try_void(SavedServer_createOrUpdate(self->queries, &server));
List_SavedServer_pushMany(&self->saved_servers, &server, 1);
try_void(showSavedServer(self, &server));
success = true;
Return RESULT_VOID;
}
static Result(void) selectServerFromCache(ClientCLI* self){
Deferral(8);
bool success = false;
u32 servers_count = self->saved_servers.len;
if(servers_count == 0){
printf("No saved servers found\n");
Return RESULT_VOID;
}
for(u32 i = 0; i < servers_count; i++){
SavedServer* server = &self->saved_servers.data[i];
printf("[%02u] "FMT_str" "FMT_str"\n",
i, str_unwrap(server->address), str_unwrap(server->name));
}
char buf[32];
u32 selected_i = -1;
while(true) {
printf("Type 'q' to cancel\n");
printf("Select server number: ");
try_void(term_readLine(buf, sizeof(buf)));
str input_line = str_from_cstr(buf);
str_trim(&input_line, true);
if(str_equals(input_line, STR("q"))){
Return RESULT_VOID;
}
if(sscanf(buf, FMT_u32, &selected_i) != 1){
printf("ERROR: not a number\n");
}
else if(selected_i >= servers_count){
printf("ERROR: not a server number\n");
}
else break;
}
SavedServer* selected_server = &self->saved_servers.data[selected_i];
printf("Connecting to '"FMT_str"'...\n", str_unwrap(selected_server->address));
try_void(Client_connect(self->client, selected_server->address.data, selected_server->pk_base64.data));
printf("Connection established\n");
// update server name
bool server_info_changed = false;
str updated_server_name = str_null;
try_void(Client_getServerName(self->client, &updated_server_name));
Defer(if(!success) str_destroy(updated_server_name));
if(!str_equals(updated_server_name, selected_server->name)){
server_info_changed = true;
selected_server->name = updated_server_name;
}
// update server description
str updated_server_description = str_null;
try_void(Client_getServerDescription(self->client, &updated_server_description));
Defer(if(!success) str_destroy(updated_server_description));
if(!str_equals(updated_server_description, selected_server->description)){
server_info_changed = true;
selected_server->description = updated_server_description;
}
if(server_info_changed){
try_void(SavedServer_createOrUpdate(self->queries, selected_server));
}
try_void(showSavedServer(self, selected_server));
success = true;
Return RESULT_VOID;
}
static Result(void) showSavedServer(ClientCLI* self, SavedServer* server){
Deferral(8);
(void)self;
printf("Server Name: "FMT_str"\n", str_unwrap(server->name));
printf("Host Address: "FMT_str"\n", str_unwrap(server->address));
printf("Description:\n"FMT_str"\n\n", str_unwrap(server->description));
printf("Public Key:\n" FMT_str"\n\n", str_unwrap(server->pk_base64));
printf("Type 'register' if you don't have an account on the server.\n");
printf("Type 'login' to authorize on the server.\n");
Return RESULT_VOID;
}
static Result(void) registerAtServer(ClientCLI* self){
Deferral(8);
i64 user_id = 0;
try_void(Client_register(self->client, &user_id));
printf("Registered successfully\n");
printf("user_id: "FMT_i64"\n", user_id);
try_assert(user_id > 0);
// TODO: use user_id somewhere
Return RESULT_VOID;
}
static Result(void) loginAtServer(ClientCLI* self){
Deferral(8);
i64 user_id = 0, landing_channel_id = 0;
try_void(Client_login(self->client, &user_id, &landing_channel_id));
printf("Authorized successfully\n");
printf("user_id: "FMT_i64", landing_channel_id: "FMT_i64"\n", user_id, landing_channel_id);
try_assert(user_id > 0);
// TODO: use user_id, landing_channel_id somewhere
Return RESULT_VOID;
}

View File

@@ -0,0 +1,195 @@
#include "ClientCLI.h"
#include "network/tcp-chat-protocol/v1.h"
#include "tlibc/filesystem.h"
static void draw_central_buttons_panel(void* data, TimRect place, bool is_selected);
static void draw_start_button(void* data, TimRect place, bool is_selected);
static void draw_exit_button(void* data, TimRect place, bool is_selected);
static Result(void) openUserDB(StartScreenContext* ctx);
#define handleError(R) _handleError(ctx, R)
static void _handleError(StartScreenContext* ctx, ResultVar(void) r){
free(ctx->err_msg);
ctx->err_msg = Error_toStr(r.error).data;
Error_free(r.error);
}
static void clearError(StartScreenContext* ctx){
free(ctx->err_msg);
ctx->err_msg = NULL;
}
void StartScreenContext_construct(StartScreenContext* ctx, ClientCLI* client){
zeroStruct(ctx);
ctx->client = client;
///////////////////////////////////////////////////////////////////////////
// input_username //
///////////////////////////////////////////////////////////////////////////
{
Array(char) username_buf = Array_char_alloc(USERNAME_SIZE_MAX + 1);
TextInputState_construct(&ctx->input_username, "[username]",
username_buf, NULL, false,
ctx->client->style.common, ctx->client->style.focused);
}
///////////////////////////////////////////////////////////////////////////
// input_password //
///////////////////////////////////////////////////////////////////////////
{
Array(char) password_buf = Array_char_alloc(PASSWORD_SIZE_MAX + 1);
TextInputState_construct(&ctx->input_password, "[password]",
password_buf, NULL, true,
ctx->client->style.common, ctx->client->style.focused);
}
///////////////////////////////////////////////////////////////////////////
// central_panel //
///////////////////////////////////////////////////////////////////////////
{
List(TimPanelItem) items = List_TimPanelItem_alloc(4);
TimPanelItem item_username_input;
TimPanelItem_fromTextInputState(&item_username_input, &ctx->input_username);
List_TimPanelItem_push(&items, item_username_input);
TimPanelItem item_password_input;
TimPanelItem_fromTextInputState(&item_password_input, &ctx->input_password);
List_TimPanelItem_push(&items, item_password_input);
TimPanelItem item_central_buttons_panel = {
.w = A, .h = 3, .data = ctx, .draw = draw_central_buttons_panel
};
List_TimPanelItem_push(&items, item_central_buttons_panel);
ctx->central_panel.items = items.data;
ctx->central_panel.len = items.len;
}
///////////////////////////////////////////////////////////////////////////
// central_buttons_panel //
///////////////////////////////////////////////////////////////////////////
{
List(TimPanelItem) items = List_TimPanelItem_alloc(4);
TimPanelItem item_start_button = {
.w = A, .h = 3, .data = ctx, .draw = draw_start_button
};
List_TimPanelItem_push(&items, item_start_button);
TimPanelItem item_exit_button = {
.w = A, .h = 3, .data = ctx, .draw = draw_exit_button
};
List_TimPanelItem_push(&items, item_exit_button);
ctx->central_buttons_panel.items = items.data;
ctx->central_buttons_panel.len = items.len;
ctx->central_buttons_panel.is_horizontal = true;
}
}
void StartScreenContext_destroy(StartScreenContext* ctx){
free(ctx->input_username.edit.s);
free(ctx->input_password.edit.s);
free(ctx->err_msg);
free(ctx->central_panel.items);
free(ctx->central_buttons_panel.items);
}
void start_screen(StartScreenContext* ctx)
{
if (tim->event.type == TimEvent_Draw) {
tim_fill(tim_cell(" ", ctx->client->style.common.fg, ctx->client->style.common.bg), 0, 0, A, A);
}
tim_frame(A, A, 40, 11, ctx->client->style.common);
tim_panel(&ctx->central_panel, true, A, A, 38, 9);
if(ctx->err_msg){
i32 below_list = tim->scopes[tim->scope].h/2 + 6;
tim_label("ERROR: ", A, below_list, A, A, ctx->client->style.error);
tim_label(ctx->err_msg, A, below_list + 1, A, A, ctx->client->style.error);
}
}
static void draw_central_buttons_panel(void* data, TimRect place, bool is_selected){
StartScreenContext* ctx = data;
tim_panel(&ctx->central_buttons_panel, is_selected, place.x, place.y, place.w, place.h);
}
static void draw_start_button(void* data, TimRect place, bool is_selected){
StartScreenContext* ctx = data;
TimStyle style = is_selected ? ctx->client->style.focused : ctx->client->style.common;
if(tim_button("[Enter] Start", place.x, place.y, place.w, A, style)
|| tim_is_key_press(TimKey_Enter))
{
clearError(ctx);
// check username
str username = str_from_cstr(ctx->input_username.edit.s);
str_trim(&username, true);
str name_error_str = validateUsername_str(username);
if(name_error_str.data){
ctx->err_msg = name_error_str.data;
return;
}
// check password
str password = str_from_cstr(ctx->input_password.edit.s);
str_trim(&password, true);
if(password.len < PASSWORD_SIZE_MIN || password.len > PASSWORD_SIZE_MAX){
ctx->err_msg = sprintf_malloc(
"password length (in bytes) must be >= %i and <= %i",
PASSWORD_SIZE_MIN, PASSWORD_SIZE_MAX
);
return;
}
// create client
try_handle(ctx->client->client, p, Client_create(username, password), handleError);
// init user DB
try_handle_void(openUserDB(ctx), handleError);
// erase password from memory
memset(ctx->input_password.edit.s, 0, ctx->input_password.edit.capacity);
// switch to next screen
ctx->client->state = ClientCLIState_MainScreen;
return;
}
}
static void draw_exit_button(void* data, TimRect place, bool is_selected){
StartScreenContext* ctx = data;
TimStyle style = is_selected ? ctx->client->style.focused : ctx->client->style.common;
if(tim_button("[Esc/Q] Exit", place.x, place.y, place.w, A, style)
|| tim_is_key_press('q')
|| tim_is_key_press(TimKey_Escape)
|| ctx->input_username.result_key == TimKey_Escape
|| ctx->input_password.result_key == TimKey_Escape)
{
ctx->client->state = ClientCLIState_Exit;
return;
}
}
static Result(void) openUserDB(StartScreenContext* ctx){
Deferral(8);
str username = Client_getUserName(ctx->client->client);
// TODO: encrypt user database
// Array(u8) user_data_key = Client_getUserDataKey(ctx->client->client);
// build database file path
try(char* user_dir, p, path_getUserDir());
Defer(free(user_dir));
char* db_path = strcat_malloc(
user_dir,
path_seps".local"path_seps"tcp-chat-client"path_seps"user-db"path_seps,
username.data, ".sqlite"
);
Defer(free(db_path));
try(ctx->client->db, p, ClientDatabase_open(db_path));
try(ctx->client->queries, p, ClientQueries_compile(ctx->client->db));
// load whole servers table to list
try_void(SavedServer_getAll(ctx->client->queries, &ctx->client->saved_servers));
Return RESULT_VOID;
}

View File

@@ -0,0 +1,32 @@
#include "ClientCLI.h"
void TextInputState_construct(TextInputState* ctx, cstr label,
Array(char) buf, NULLABLE(cstr) initial_value, bool masked,
TimStyle common, TimStyle focused)
{
TimEditState_construct(&ctx->edit, buf.data, buf.len, initial_value);
ctx->edit.masked = masked;
ctx->label = label;
ctx->style.common = common;
ctx->style.focused = focused;
}
void text_input(TextInputState* ctx, i32 x, i32 y, i32 w, TimStyle style){
ctx->result_key = tim_edit(&ctx->edit, x, y, w, style);
tim_label(ctx->label, x + 3, y, A, 1, style);
}
void TimPanelItem_fromTextInputState(TimPanelItem* item, TextInputState* input){
zeroStruct(item);
item->w = A;
item->h = 3;
item->data = input;
item->focus_target = &input->edit;
item->draw = draw_item_text_input;
}
void draw_item_text_input(void* data, TimRect place, bool is_selected){
TextInputState* ctx = data;
TimStyle style = is_selected ? ctx->style.focused : ctx->style.common;
text_input(ctx, place.x, place.y, place.w, style);
}

View File

@@ -1,6 +1,6 @@
#include "tcp-chat.h"
#include "tlibc/tlibc.h"
#include "tlibtoml.h"
#include "tcp-chat/tcp-chat.h"
#include "cryptography/RSA.h"
#include "cli/modes/modes.h"

View File

@@ -9,7 +9,7 @@ Result(void) run_ClientMode(cstr config_path) {
ClientCLI_construct(&client);
Defer(ClientCLI_destroy(&client));
// start infinite loop on main thread
try_void(ClientCLI_run(&client));
ClientCLI_run(&client);
Return RESULT_VOID;
}

View File

@@ -1,8 +1,8 @@
#include <pthread.h>
#include "tcp-chat.h"
#include "modes.h"
#include "tcp-chat/server.h"
#include "tlibc/time.h"
#include "tlibc/term.h"
#include <pthread.h>
typedef struct ServerLogger {
pthread_mutex_t mutex;

View File

@@ -8,8 +8,7 @@ void ServerConnection_close(ServerConnection* self){
EncryptedSocketTCP_destroy(&self->sock);
Array_u8_destroy(&self->token);
Array_u8_destroy(&self->session_key);
str_destroy(self->server_name);
str_destroy(self->server_description);
MessageBlock_destroy(&self->received_message_block);
free(self);
}
@@ -75,16 +74,13 @@ Result(ServerConnection*) ServerConnection_open(Client* client, cstr server_addr
PacketType_ServerHandshake));
conn->session_id = server_handshake.session_id;
// get server name
try_void(ServerConnection_requestServerName(conn));
// get server description
try_void(ServerConnection_requestServerDescription(conn));
MessageBlock_alloc(&conn->received_message_block);
success = true;
Return RESULT_VALUE(p, conn);
}
Result(void) ServerConnection_requestServerName(ServerConnection* conn){
Result(void) ServerConnection_requestServerName(ServerConnection* conn, str* out_str){
if(conn == NULL){
return RESULT_ERROR_LITERAL("Client is not connected to a server");
}
@@ -98,12 +94,12 @@ Result(void) ServerConnection_requestServerName(ServerConnection* conn){
try_void(sendRequest(&conn->sock, &req_header, &public_info_req));
try_void(recvResponse(&conn->sock, &res_header, &public_info_res,
PacketType_ServerPublicInfoResponse));
try_void(recvStr(&conn->sock, public_info_res.data_size, &conn->server_name));
try_void(recvStr(&conn->sock, public_info_res.data_size, out_str));
Return RESULT_VOID;
}
Result(void) ServerConnection_requestServerDescription(ServerConnection* conn){
Result(void) ServerConnection_requestServerDescription(ServerConnection* conn, str* out_str){
if(conn == NULL){
return RESULT_ERROR_LITERAL("Client is not connected to a server");
}
@@ -117,7 +113,7 @@ Result(void) ServerConnection_requestServerDescription(ServerConnection* conn){
try_void(sendRequest(&conn->sock, &req_header, &public_info_req));
try_void(recvResponse(&conn->sock, &res_header, &public_info_res,
PacketType_ServerPublicInfoResponse));
try_void(recvStr(&conn->sock, public_info_res.data_size, &conn->server_description));
try_void(recvStr(&conn->sock, public_info_res.data_size, out_str));
Return RESULT_VOID;
}

View File

@@ -52,27 +52,27 @@ Array(u8) Client_getUserDataKey(Client* client){
return client->user_data_key;
}
Result(void) Client_getServerName(Client* self, str* out_name){
Result(void) Client_getServerName(Client* self, str* out_str){
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
*out_name = self->conn->server_name;
try_void(ServerConnection_requestServerName(self->conn, out_str));
Return RESULT_VOID;
}
Result(void) Client_getServerDescription(Client* self, str* out_desc){
Result(void) Client_getServerDescription(Client* self, str* out_str){
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
*out_desc = self->conn->server_description;
try_void(ServerConnection_requestServerDescription(self->conn, out_str));
Return RESULT_VOID;
}
Result(void) Client_register(Client* self, u64* out_user_id){
Result(void) Client_register(Client* self, i64* out_user_id){
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
@@ -80,7 +80,6 @@ Result(void) Client_register(Client* self, u64* out_user_id){
PacketHeader req_head, res_head;
RegisterRequest req;
RegisterResponse res;
// TODO: hash token with server public key
try_void(RegisterRequest_tryConstruct(&req, &req_head, self->username, self->conn->token));
try_void(sendRequest(&self->conn->sock, &req_head, &req));
try_void(recvResponse(&self->conn->sock, &res_head, &res, PacketType_RegisterResponse));
@@ -90,7 +89,7 @@ Result(void) Client_register(Client* self, u64* out_user_id){
Return RESULT_VOID;
}
Result(void) Client_login(Client* self, u64* out_user_id, u64* out_landing_channel_id){
Result(void) Client_login(Client* self, i64* out_user_id, i64* out_landing_channel_id){
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
@@ -98,7 +97,6 @@ Result(void) Client_login(Client* self, u64* out_user_id, u64* out_landing_chann
PacketHeader req_head, res_head;
LoginRequest req;
LoginResponse res;
// TODO: hash token with server public key
try_void(LoginRequest_tryConstruct(&req, &req_head, self->username, self->conn->token));
try_void(sendRequest(&self->conn->sock, &req_head, &req));
try_void(recvResponse(&self->conn->sock, &res_head, &res, PacketType_LoginResponse));
@@ -108,3 +106,71 @@ Result(void) Client_login(Client* self, u64* out_user_id, u64* out_landing_chann
Return RESULT_VOID;
}
Result(i64) Client_sendMessage(Client* self, i64 channel_id, Array(u8) content, DateTime* out_timestamp){
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
try_assert(content.len >= MESSAGE_SIZE_MIN && content.len <= MESSAGE_SIZE_MAX);
PacketHeader req_head, res_head;
SendMessageRequest req;
SendMessageResponse res;
SendMessageRequest_construct(&req, &req_head, channel_id, content.len);
try_void(sendRequest(&self->conn->sock, &req_head, &req));
try_void(recvResponse(&self->conn->sock, &res_head, &res, PacketType_SendMessageResponse));
*out_timestamp = res.timestamp;
Return RESULT_VALUE(i, res.message_id);
}
Result(u32) Client_receiveMessageBlock(Client* self, i64 channel_id, i64 first_message_id, u32 messages_count){
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
PacketHeader req_head, res_head;
GetMessageBlockRequest req;
GetMessageBlockResponse res;
GetMessageBlockRequest_construct(&req, &req_head, channel_id, first_message_id, messages_count);
try_void(sendRequest(&self->conn->sock, &req_head, &req));
try_void(recvResponse(&self->conn->sock, &res_head, &res, PacketType_GetMessageBlockResponse));
self->conn->received_message_block.messages_count = res.messages_count;
self->conn->received_message_block.datum.len = res.data_size;
try_void(
EncryptedSocketTCP_recv(
&self->conn->sock,
self->conn->received_message_block.datum,
SocketRecvFlag_WholeBuffer
)
);
Return RESULT_VALUE(u, res.messages_count);
}
Result(u32) Client_popMessage(Client* self, Array(u8) dst_content,
i64* message_id, i64* sender_id, DateTime* timestamp_utc)
{
Deferral(1);
try_assert(self != NULL);
try_assert(self->conn != NULL && "didn't connect to a server yet");
try_assert(dst_content.len >= MESSAGE_SIZE_MAX);
MessageMeta msg_meta = {0};
try(bool read_success, u,
MessageBlock_readMessage(
&self->conn->received_message_block,
&msg_meta,
dst_content
)
);
if(!read_success){
Return RESULT_VALUE(u, 0);
}
*message_id = msg_meta.id;
*sender_id = msg_meta.sender_id;
*timestamp_utc = msg_meta.timestamp;
Return RESULT_VALUE(u, msg_meta.data_size);
}

View File

@@ -1,8 +1,9 @@
#pragma once
#include "tcp-chat/client.h"
#include "tcp-chat.h"
#include "cryptography/AES.h"
#include "cryptography/RSA.h"
#include "network/encrypted_sockets.h"
#include "network/tcp-chat-protocol/v1.h"
typedef struct ServerConnection ServerConnection;
@@ -12,7 +13,6 @@ typedef struct Client {
ServerConnection* conn;
} Client;
typedef struct ServerConnection {
Client* client;
EndpointIPv4 server_end;
@@ -21,10 +21,9 @@ typedef struct ServerConnection {
Array(u8) token;
Array(u8) session_key;
EncryptedSocketTCP sock;
u64 session_id;
str server_name;
str server_description;
u64 user_id;
i64 session_id;
i64 user_id;
MessageBlock received_message_block;
} ServerConnection;
/// @param server_addr_cstr
@@ -34,8 +33,8 @@ Result(ServerConnection*) ServerConnection_open(Client* client,
void ServerConnection_close(ServerConnection* conn);
/// updates conn->server_name
Result(void) ServerConnection_requestServerName(ServerConnection* conn);
/// @param out_str heap-allocated string
Result(void) ServerConnection_requestServerName(ServerConnection* conn, str* out_str);
/// updates conn->server_description
Result(void) ServerConnection_requestServerDescription(ServerConnection* conn);
/// @param out_str heap-allocated string
Result(void) ServerConnection_requestServerDescription(ServerConnection* conn, str* out_str);

View File

@@ -1,6 +1,6 @@
#include "requests.h"
Result(void) recvStr(EncryptedSocketTCP* sock, u32 size, str* out_s){
Result(void) recvStr(EncryptedSocketTCP* sock, u32 size, str* out_str){
Deferral(4);
str s = str_construct(malloc(size + 1), size, true);
@@ -17,7 +17,7 @@ Result(void) recvStr(EncryptedSocketTCP* sock, u32 size, str* out_s){
);
s.data[s.len] = 0;
*out_s = s;
*out_str = s;
success = true;
Return RESULT_VOID;
}

View File

@@ -3,10 +3,12 @@
#include "client/client_internal.h"
/// @param out_err_msg heap-allocated string
Result(void) recvErrorMessage(EncryptedSocketTCP* sock, PacketHeader* res_header,
str* out_err_msg);
Result(void) recvStr(EncryptedSocketTCP* sock, u32 size, str* out_s);
/// @param out_str heap-allocated string
Result(void) recvStr(EncryptedSocketTCP* sock, u32 size, str* out_str);
Result(void) _recvResponse(EncryptedSocketTCP* sock,
PacketHeader* res_header, Array(u8) res, PacketType res_type);

View File

@@ -1,7 +1,5 @@
#include "RSA.h"
#include <assert.h>
#include "bearssl_x509.h"
#include "bearssl_pem.h"
#include "tlibc/base64.h"
// https://crypto.stackexchange.com/questions/3110/impacts-of-not-using-rsa-exponent-of-65537

View File

@@ -1,10 +1,9 @@
#pragma once
#include "tlibc/errors.h"
#include "tcp-chat.h"
#include "tlibc/collections/Array.h"
#include "tlibc/collections/Array_impl/Array_u8.h"
#include "bearssl_rand.h"
#include "bearssl_hash.h"
#include "tcp-chat/common_constants.h"
//////////////////////////////////////////////////////////////////////////////
// //

View File

@@ -1,5 +1,5 @@
#pragma once
#include "tcp-chat/tcp-chat.h"
#include "tcp-chat.h"
#include "endpoint.h"
#if !defined(KN_USE_WINSOCK)

View File

@@ -132,12 +132,12 @@ Result(i32) socket_recvfrom(Socket s, Array(u8) buffer, SocketRecvFlag flags, NU
}
Result(void) socket_TCP_enableAliveChecks(Socket s,
sec_t first_check_time, u32 checks_count, sec_t checks_interval)
sec_t first_check_time, u32 check_count, sec_t checks_interval)
{
#if KN_USE_WINSOCK
BOOL opt_SO_KEEPALIVE = 1; // enable keepalives
DWORD opt_TCP_KEEPIDLE = first_check_time;
DWORD opt_TCP_KEEPCNT = checks_count;
DWORD opt_TCP_KEEPCNT = check_count;
DWORD opt_TCP_KEEPINTVL = checks_interval;
try_setsockopt(s, SOL_SOCKET, SO_KEEPALIVE);
try_setsockopt(s, IPPROTO_TCP, TCP_KEEPIDLE);
@@ -145,12 +145,12 @@ Result(void) socket_TCP_enableAliveChecks(Socket s,
try_setsockopt(s, IPPROTO_TCP, TCP_KEEPINTVL);
// timeout for connect()
DWORD opt_TCP_MAXRT = checks_count * checks_interval;
DWORD opt_TCP_MAXRT = check_count * checks_interval;
try_setsockopt(s, IPPROTO_TCP, TCP_MAXRT);
#else
int opt_SO_KEEPALIVE = 1; // enable keepalives
int opt_TCP_KEEPIDLE = first_check_time;
int opt_TCP_KEEPCNT = checks_count;
int opt_TCP_KEEPCNT = check_count;
int opt_TCP_KEEPINTVL = checks_interval;
try_setsockopt(s, SOL_SOCKET, SO_KEEPALIVE);
try_setsockopt(s, IPPROTO_TCP, TCP_KEEPIDLE);
@@ -158,7 +158,7 @@ Result(void) socket_TCP_enableAliveChecks(Socket s,
try_setsockopt(s, IPPROTO_TCP, TCP_KEEPINTVL);
// read more in the article
int opt_TCP_USER_TIMEOUT = checks_count * checks_interval * 1000;
int opt_TCP_USER_TIMEOUT = check_count * checks_interval * 1000;
try_setsockopt(s, IPPROTO_TCP, TCP_USER_TIMEOUT);
#endif
return RESULT_VOID;

View File

@@ -38,7 +38,7 @@ Result(i32) socket_recvfrom(Socket s, Array(u8) buffer, SocketRecvFlag flags
/// Read more: https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die/
/// RU translaton: https://habr.com/ru/articles/700470/
Result(void) socket_TCP_enableAliveChecks(Socket s,
sec_t first_check_time, u32 checks_count, sec_t checks_interval);
sec_t first_check_time, u32 check_count, sec_t checks_interval);
#define socket_TCP_enableAliveChecks_default(socket) \
socket_TCP_enableAliveChecks(socket, 1, 4, 5)

View File

@@ -1,7 +1,6 @@
#pragma once
#include "tlibc/errors.h"
#include "tcp-chat.h"
#include "tlibc/magic.h"
#include "tcp-chat/common_constants.h"
#define AES_SESSION_KEY_SIZE 32

View File

@@ -19,7 +19,7 @@ str validateUsername_str(str username){
if(username.len < USERNAME_SIZE_MIN || username.len > USERNAME_SIZE_MAX){
return str_from_cstr(
sprintf_malloc(
"username length (in bytes) must be >= %i and <= %i\n",
"username length (in bytes) must be >= %i and <= %i",
USERNAME_SIZE_MIN, USERNAME_SIZE_MAX
)
);
@@ -63,7 +63,7 @@ Result(void) ClientHandshake_tryConstruct(ClientHandshake* ptr, PacketHeader* he
}
void ServerHandshake_construct(ServerHandshake* ptr, PacketHeader* header,
u64 session_id)
i64 session_id)
{
_PacketHeader_construct(ServerHandshake);
zeroStruct(ptr);
@@ -106,7 +106,7 @@ Result(void) LoginRequest_tryConstruct(LoginRequest *ptr, PacketHeader* header,
}
void LoginResponse_construct(LoginResponse* ptr, PacketHeader* header,
u64 user_id, u64 landing_channel_id)
i64 user_id, i64 landing_channel_id)
{
_PacketHeader_construct(LoginResponse);
zeroStruct(ptr);
@@ -135,15 +135,78 @@ Result(void) RegisterRequest_tryConstruct(RegisterRequest *ptr, PacketHeader* he
}
void RegisterResponse_construct(RegisterResponse *ptr, PacketHeader* header,
u64 user_id)
i64 user_id)
{
_PacketHeader_construct(RegisterResponse);
zeroStruct(ptr);
ptr->user_id = user_id;
}
Result(bool) MessageBlock_writeMessage(MessageBlock* block,
const MessageMeta* msg_meta, const Array(u8) msg_content)
{
Deferral(1);
// check msg_meta
try_assert(msg_meta->magic.n == MESSAGE_MAGIC.n);
try_assert(msg_meta->data_size >= MESSAGE_SIZE_MIN && msg_meta->data_size <= MESSAGE_SIZE_MAX);
try_assert(msg_meta->data_size <= msg_content.len);
try_assert(msg_meta->id > 0);
try_assert(msg_meta->sender_id > 0);
try_assert(msg_meta->timestamp.d.year > 2024);
// check block->datum.len
if(block->datum.len < block->offset + sizeof(MessageMeta) + msg_meta->data_size){
Return RESULT_VALUE(u, false);
}
// write msg_meta
memcpy(block->datum.data + block->offset, msg_meta, sizeof(MessageMeta));
block->offset += sizeof(MessageMeta);
// write msg_content
memcpy(block->datum.data + block->offset, msg_content.data, msg_meta->data_size);
block->offset += msg_meta->data_size;
block->messages_count++;
Return RESULT_VALUE(u, true);
}
Result(bool) MessageBlock_readMessage(MessageBlock* block,
MessageMeta* msg_meta, Array(u8) msg_content)
{
Deferral(1);
// check block
if(block->messages_count == 0){
Return RESULT_VALUE(u, false);
}
try_assert(block->datum.len >= block->offset + sizeof(MessageMeta) + MESSAGE_SIZE_MIN);
// check msg_content.len
try_assert(msg_content.len >= MESSAGE_SIZE_MAX);
// read msg_meta
memcpy(msg_meta, block->datum.data + block->offset, sizeof(MessageMeta));
block->offset += sizeof(MessageMeta);
// check msg_meta
try_assert(msg_meta->magic.n == MESSAGE_MAGIC.n);
try_assert(msg_meta->data_size >= MESSAGE_SIZE_MIN && msg_meta->data_size <= MESSAGE_SIZE_MAX);
try_assert(msg_meta->data_size <= msg_content.len);
try_assert(msg_meta->id > 0);
try_assert(msg_meta->sender_id > 0);
try_assert(msg_meta->timestamp.d.year > 2024);
try_assert(block->datum.len >= block->offset + msg_meta->data_size);
// read msg_content
memcpy(msg_content.data, block->datum.data + block->offset, msg_meta->data_size);
block->offset += msg_meta->data_size;
block->messages_count--;
Return RESULT_VALUE(u, true);
}
void SendMessageRequest_construct(SendMessageRequest *ptr, PacketHeader *header,
u64 channel_id, u16 data_size)
i64 channel_id, u16 data_size)
{
_PacketHeader_construct(SendMessageRequest);
zeroStruct(ptr);
@@ -152,16 +215,16 @@ void SendMessageRequest_construct(SendMessageRequest *ptr, PacketHeader *header,
}
void SendMessageResponse_construct(SendMessageResponse *ptr, PacketHeader *header,
u64 message_id, DateTime receiving_time_utc)
i64 message_id, DateTime timestamp)
{
_PacketHeader_construct(SendMessageResponse);
zeroStruct(ptr);
ptr->message_id = message_id;
ptr->receiving_time_utc = receiving_time_utc;
ptr->timestamp = timestamp;
}
void GetMessageBlockRequest_construct(GetMessageBlockRequest *ptr, PacketHeader *header,
u64 channel_id, u64 first_message_id, u32 messages_count)
i64 channel_id, i64 first_message_id, u32 messages_count)
{
_PacketHeader_construct(GetMessageBlockRequest);
zeroStruct(ptr);
@@ -171,11 +234,10 @@ void GetMessageBlockRequest_construct(GetMessageBlockRequest *ptr, PacketHeader
}
void GetMessageBlockResponse_construct(GetMessageBlockResponse *ptr, PacketHeader *header,
u64 first_message_id, u32 messages_count, u32 data_size)
u32 messages_count, u32 data_size)
{
_PacketHeader_construct(GetMessageBlockResponse);
zeroStruct(ptr);
ptr->first_message_id = first_message_id;
ptr->messages_count = messages_count;
ptr->data_size = data_size;
}

View File

@@ -1,6 +1,5 @@
#pragma once
#include "tlibc/errors.h"
#include "tlibc/string/str.h"
#include "tcp-chat.h"
#include "tlibc/time.h"
#include "network/tcp-chat-protocol/constant.h"
@@ -60,11 +59,11 @@ Result(void) ClientHandshake_tryConstruct(ClientHandshake* ptr, PacketHeader* he
typedef struct ServerHandshake {
u64 session_id;
i64 session_id;
} ALIGN_PACKET_STRUCT ServerHandshake;
void ServerHandshake_construct(ServerHandshake* ptr, PacketHeader* header,
u64 session_id);
i64 session_id);
typedef enum ServerPublicInfo {
@@ -99,12 +98,12 @@ Result(void) LoginRequest_tryConstruct(LoginRequest* ptr, PacketHeader* header,
typedef struct LoginResponse {
u64 user_id;
u64 landing_channel_id;
i64 user_id;
i64 landing_channel_id;
} ALIGN_PACKET_STRUCT LoginResponse;
void LoginResponse_construct(LoginResponse* ptr, PacketHeader* header,
u64 user_id, u64 landing_channel_id);
i64 user_id, i64 landing_channel_id);
typedef struct RegisterRequest {
@@ -117,49 +116,114 @@ Result(void) RegisterRequest_tryConstruct(RegisterRequest* ptr, PacketHeader* he
typedef struct RegisterResponse {
u64 user_id;
i64 user_id;
} ALIGN_PACKET_STRUCT RegisterResponse;
void RegisterResponse_construct(RegisterResponse* ptr, PacketHeader* header,
u64 user_id);
i64 user_id);
typedef struct MessageMeta {
Magic32 magic;
u16 data_size;
i64 id;
i64 sender_id;
DateTime timestamp; /* UTC */
} ATTRIBUTE_ALIGNED(8) MessageMeta;
#define MessageMeta_construct(DATA_SIZE, MESSAGE_ID, SENDER_ID, TIMESTAMP) ((MessageMeta){ \
.magic = MESSAGE_MAGIC, \
.data_size = DATA_SIZE, \
.id = MESSAGE_ID, \
.sender_id = SENDER_ID, \
.timestamp = TIMESTAMP \
})
#define MESSAGE_MAGIC ((Magic32){ .bytes = { 'M', 's', 'g', '1' } })
typedef struct MessageBlock {
Array(u8) datum; // sequence(MessageMeta, byte[MessageMeta.data_size])
u32 messages_count;
u32 offset;
} MessageBlock;
static inline void MessageBlock_construct(MessageBlock* self, Array(u8) datum, u32 messages_count){
self->datum = datum;
self->messages_count = messages_count;
self->offset = 0;
}
static inline void MessageBlock_alloc(MessageBlock* self){
self->datum = Array_u8_alloc(MESSAGE_BLOCK_COUNT_MAX * (sizeof(MessageMeta) + MESSAGE_SIZE_MAX));
Array_u8_memset(&self->datum, 0);
self->messages_count = 0;
self->offset = 0;
}
static inline void MessageBlock_reset(MessageBlock* self){
Array_u8_memset(&self->datum, 0);
self->messages_count = 0;
self->offset = 0;
}
static inline void MessageBlock_destroy(MessageBlock* self){
if(!self)
return;
Array_u8_destroy(&self->datum);
}
/// @brief write msg_meta and msg_meta->data_size bytes from msg_content to block and increase block.messages_count
/// @param block use MessageBlock_alloc() to create empty block
/// @param msg_meta use MessageMeta_construct() to create message metadata
/// @param msg_content array of size >= msg_meta.data_size
/// @return false if msg_meta and msg_content don't fit in block.datum
Result(bool) MessageBlock_writeMessage(MessageBlock* block,
const MessageMeta* msg_meta, const Array(u8) msg_content);
/// @brief read msg_meta and msg_content from block and decrease block.messages_count
/// @param block a block with correct .datum and .messages_count
/// @param msg_meta out meta copied from block_data
/// @param msg_content out content copied from block_data. Array of size >= MESSAGE_SIZE_MAX
/// @return false if there are no messages to read (block.messages_count == 0)
Result(bool) MessageBlock_readMessage(MessageBlock* block,
MessageMeta* msg_meta, Array(u8) msg_content);
typedef struct SendMessageRequest {
u64 channel_id;
i64 channel_id;
u16 data_size;
/* stream of size data_size */
} ALIGN_PACKET_STRUCT SendMessageRequest;
void SendMessageRequest_construct(SendMessageRequest* ptr, PacketHeader* header,
u64 channel_id, u16 data_size);
i64 channel_id, u16 data_size);
typedef struct SendMessageResponse {
u64 message_id;
DateTime receiving_time_utc;
i64 message_id;
DateTime timestamp; /* UTC */
} ALIGN_PACKET_STRUCT SendMessageResponse;
void SendMessageResponse_construct(SendMessageResponse* ptr, PacketHeader* header,
u64 message_id, DateTime receiving_time_utc);
i64 message_id, DateTime timestamp);
typedef struct GetMessageBlockRequest {
u64 channel_id;
u64 first_message_id;
i64 channel_id;
i64 first_message_id;
u32 messages_count;
} ALIGN_PACKET_STRUCT GetMessageBlockRequest;
void GetMessageBlockRequest_construct(GetMessageBlockRequest* ptr, PacketHeader* header,
u64 channel_id, u64 first_message_id, u32 messages_count);
i64 channel_id, i64 first_message_id, u32 messages_count);
typedef struct GetMessageBlockResponse {
u64 first_message_id;
u32 messages_count;
u32 data_size;
/* stream of size data_size : ((sequence MessageMeta), (sequence binary-data)) */
/* stream of size data_size : sequence (MessageMeta, byte[MessageMeta.data_size]) */
} ALIGN_PACKET_STRUCT GetMessageBlockResponse;
void GetMessageBlockResponse_construct(GetMessageBlockResponse* ptr, PacketHeader* header,
u64 first_message_id, u32 messages_count, u32 data_size);
u32 messages_count, u32 data_size);

View File

@@ -1,186 +0,0 @@
#include "server_internal.h"
#include "tlibc/string/StringBuilder.h"
#include "tlibc/filesystem.h"
void Channel_free(Channel* self){
if(!self)
return;
str_destroy(self->name);
str_destroy(self->description);
List_MessageBlockMeta_destroy(&self->messages.blocks_meta_list);
LList_MessageBlock_destroy(&self->messages.blocks_queue);
free(self);
}
Result(Channel*) Channel_create(u64 chan_id, str name, str description,
IncrementalDB* db, bool lock_db)
{
Deferral(8);
Channel* self = (Channel*)malloc(sizeof(Channel));
zeroStruct(self);
bool success = false;
Defer(if(!success) Channel_free(self));
self->id = chan_id;
try_assert(name.len >= CHANNEL_NAME_SIZE_MIN && name.len <= CHANNEL_NAME_SIZE_MAX);
self->name = str_copy(name);
try_assert(description.len <= CHANNEL_DESC_SIZE_MAX);
self->description = str_copy(description);
if(lock_db){
idb_lockDB(db);
Defer(idb_unlockDB(db));
}
StringBuilder sb = StringBuilder_alloc(CHANNEL_NAME_SIZE_MAX + 32 + 1);
Defer(StringBuilder_destroy(&sb));
StringBuilder_append_str(&sb, STR("channels"));
StringBuilder_append_char(&sb, path_sep);
StringBuilder_append_str(&sb, name);
str subdir = str_copy(StringBuilder_getStr(&sb));
Defer(str_destroy(subdir));
str message_blocks_str = STR("message_blocks");
str message_blocks_meta_str = STR("message_blocks_meta");
StringBuilder_removeFromEnd(&sb, -1);
StringBuilder_append_str(&sb, name);
StringBuilder_append_char(&sb, '_');
StringBuilder_append_str(&sb, message_blocks_str);
try(self->messages.blocks_table, p,
idb_getOrCreateTable(db, subdir, StringBuilder_getStr(&sb), sizeof(MessageBlock), false)
);
idb_lockTable(self->messages.blocks_table);
Defer(idb_unlockTable(self->messages.blocks_table));
StringBuilder_removeFromEnd(&sb, message_blocks_str.len);
StringBuilder_append_str(&sb, message_blocks_meta_str);
try(self->messages.blocks_meta_table, p,
idb_getOrCreateTable(db, subdir, StringBuilder_getStr(&sb), sizeof(MessageBlockMeta), false)
);
idb_lockTable(self->messages.blocks_meta_table);
Defer(idb_unlockTable(self->messages.blocks_meta_table));
// load whole message_blocks_meta table to list
try_void(
idb_createListFromTable(self->messages.blocks_meta_table, (void*)&self->messages.blocks_meta_list, false)
);
// load N last blocks to the queue
self->messages.blocks_queue = LList_construct(MessageBlock, NULL);
u64 message_blocks_count = self->messages.blocks_meta_list.len;
u64 first_block_id = 0;
if(message_blocks_count > MESSAGE_BLOCKS_CACHE_COUNT)
first_block_id = message_blocks_count - MESSAGE_BLOCKS_CACHE_COUNT;
for(u64 id = first_block_id; id < message_blocks_count; id++){
LLNode(MessageBlock)* node = LLNode_MessageBlock_createZero();
LList_MessageBlock_insertAfter(
&self->messages.blocks_queue,
self->messages.blocks_queue.last,
node
);
try_void(idb_getRow(self->messages.blocks_table, id, node->value.data, false));
}
if(self->messages.blocks_meta_list.len > 0){
MessageBlockMeta last_block_meta = self->messages.blocks_meta_list.data[self->messages.blocks_meta_list.len - 1];
self->messages.count = last_block_meta.first_message_id + last_block_meta.messages_count - 1;
}
else {
self->messages.count = 0;
}
success = true;
Return RESULT_VALUE(p, self);
}
void Channel_unloadExcessBlocks(Channel* self){
while(self->messages.blocks_queue.count > MESSAGE_BLOCKS_CACHE_COUNT){
LLNode(MessageBlock)* node = self->messages.blocks_queue.first;
LList_MessageBlock_detatch(&self->messages.blocks_queue, node);
free(node);
}
}
Result(void) Channel_saveMessage(Channel* self, Array(u8) message_data, u64 sender_id,
MessageMeta* out_message_meta, bool lock_tables)
{
Deferral(4);
if(lock_tables){
idb_lockTable(self->messages.blocks_table);
idb_lockTable(self->messages.blocks_meta_table);
Defer(
idb_unlockTable(self->messages.blocks_table);
idb_unlockTable(self->messages.blocks_meta_table);
);
}
// create new block if message won't fit in the last existing
MessageBlockMeta* incomplete_block_meta = self->messages.blocks_meta_list.data + self->messages.blocks_meta_list.len;
u64 new_message_id = incomplete_block_meta->first_message_id + incomplete_block_meta->messages_count;
u32 message_size_in_block = sizeof(MessageMeta) + ALIGN_TO(message_data.len, 8);
if(incomplete_block_meta->data_size + message_size_in_block > MESSAGE_BLOCK_SIZE){
// create new MessageBlockMeta
incomplete_block_meta = List_MessageBlockMeta_expand(&self->messages.blocks_meta_list, 1);
incomplete_block_meta->first_message_id = new_message_id;
incomplete_block_meta->messages_count = 0;
incomplete_block_meta->data_size = 0;
// create new MessageBlock
LList_MessageBlock_insertAfter(
&self->messages.blocks_queue,
self->messages.blocks_queue.last,
LLNode_MessageBlock_createZero());
// unload old blocks from cache
Channel_unloadExcessBlocks(self);
}
// create message meta
out_message_meta->magic = MESSAGE_MAGIC;
out_message_meta->data_size = message_data.len;
out_message_meta->id = new_message_id;
out_message_meta->sender_id = sender_id;
DateTime_getUTC(&out_message_meta->receiving_time_utc);
// copy message data to message block
MessageBlock* incomplete_block = &self->messages.blocks_queue.last->value;
u8* data_ptr = incomplete_block->data + incomplete_block_meta->data_size;
memcpy(data_ptr, out_message_meta, sizeof(MessageMeta));
data_ptr += sizeof(MessageMeta);
memcpy(data_ptr, message_data.data, message_data.len);
incomplete_block_meta->data_size += sizeof(MessageMeta) + ALIGN_TO(message_data.len, 8);
incomplete_block_meta->messages_count++;
// save to DB
try_void(idb_pushRow(self->messages.blocks_meta_table, incomplete_block_meta, false));
try_void(idb_pushRow(self->messages.blocks_table, incomplete_block, false));
Return RESULT_VOID;
}
Result(void) Channel_loadMessageBlock(Channel* self, u64 fisrt_message_id, u32 count,
MessageBlockMeta* out_meta, NULLABLE(Array(u8)*) out_block, bool lock_tables)
{
Deferral(4);
if(lock_tables){
idb_lockTable(self->messages.blocks_table);
idb_lockTable(self->messages.blocks_meta_table);
Defer(
idb_unlockTable(self->messages.blocks_table);
idb_unlockTable(self->messages.blocks_meta_table);
);
}
// TODO: Maybe it's better to request message block id directly? Client doesn't know how much bytes `count` messages will take, this can lead to severe lags on slow internet
// TODO: binary search in list of blocks meta
// TODO: return if out_block == NULL
// TODO: check if block is in N_LAST_BLOCKS
// TODO: load block
// TODO: insert block in queue and keep it sorted
Return RESULT_VOID;
}

View File

@@ -1,11 +1,14 @@
#include "server/server_internal.h"
#include "network/tcp-chat-protocol/v1.h"
void ClientConnection_close(ClientConnection* conn){
if(!conn)
return;
EncryptedSocketTCP_destroy(&conn->sock);
Array_u8_destroy(&conn->session_key);
MessageBlock_destroy(&conn->message_block);
Array_u8_destroy(&conn->message_content);
ServerQueries_free(conn->queries);
tsqlite_connection_close(conn->db);
free(conn);
}
@@ -21,10 +24,17 @@ Result(ClientConnection*) ClientConnection_accept(ConnectionHandlerArgs* args)
conn->server = args->server;
conn->client_end = args->client_end;
conn->session_id = args->session_id;
conn->authorized = false;
conn->user_id = -1;
conn->session_key = Array_u8_alloc(AES_SESSION_KEY_SIZE);
// buffers
MessageBlock_alloc(&conn->message_block);
conn->message_content = Array_u8_alloc(MESSAGE_SIZE_MAX);
// database
try(conn->db, p, tsqlite_connection_open(args->server->db_path));
try(conn->queries, p, ServerQueries_compile(conn->db));
// correct session key will be received from client later
conn->session_key = Array_u8_alloc(AES_SESSION_KEY_SIZE);
Array_u8_memset(&conn->session_key, 0);
EncryptedSocketTCP_construct(&conn->sock, args->accepted_socket_tcp, NETWORK_BUFFER_SIZE, conn->session_key);
try_void(socket_TCP_enableAliveChecks_default(args->accepted_socket_tcp));

110
src/server/db/Channel.c Normal file
View File

@@ -0,0 +1,110 @@
#include "server_db_internal.h"
Result(bool) Channel_exists(ServerQueries* q, i64 id){
Deferral(4);
tsqlite_statement* st = q->channels.exists;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_i64(st, "$id", id));
try(bool has_result, i, tsqlite_statement_step(st));
Return RESULT_VALUE(i, has_result);
}
Result(bool) Channel_createOrUpdate(ServerQueries* q,
i64 id, str name, str description)
{
Deferral(4);
try_assert(id > 0);
try_assert(name.len >= CHANNEL_NAME_SIZE_MIN && name.len <= CHANNEL_NAME_SIZE_MAX);
try_assert(description.len <= CHANNEL_DESC_SIZE_MAX);
try(bool channel_exists, i, Channel_exists(q, id));
tsqlite_statement* st = NULL;
Defer(tsqlite_statement_reset(st));
if(channel_exists){
st = q->channels.update;
}
else {
st = q->channels.insert;
}
try_void(tsqlite_statement_bind_i64(st, "$id", id));
try_void(tsqlite_statement_bind_str(st, "$name", name, NULL));
try_void(tsqlite_statement_bind_str(st, "$description", description, NULL));
try_void(tsqlite_statement_step(st));
Return RESULT_VALUE(i, !channel_exists);
}
Result(void) Channel_saveMessage(ServerQueries* q,
i64 channel_id, i64 sender_id, Array(u8) content,
DateTime* out_timestamp)
{
Deferral(4);
try_assert(content.len >= MESSAGE_SIZE_MIN && content.len <= MESSAGE_SIZE_MAX);
tsqlite_statement* st = q->messages.insert;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_i64(st, "$channel_id", channel_id));
try_void(tsqlite_statement_bind_i64(st, "$sender_id", sender_id));
try_void(tsqlite_statement_bind_blob(st, "$content", content, NULL));
try(bool has_result, i, tsqlite_statement_step(st));
try_assert(has_result);
try(i64 message_id, i, tsqlite_statement_getResult_i64(st));
str timestamp_str;
try_void(tsqlite_statement_getResult_str(st, &timestamp_str));
try_void(DateTime_parse(timestamp_str.data, out_timestamp));
Return RESULT_VALUE(i, message_id);
}
Result(void) Channel_loadMessageBlock(ServerQueries* q,
i64 channel_id, i64 first_message_id, u32 count,
MessageBlock* block)
{
Deferral(4);
if(count == 0){
Return RESULT_VOID;
}
try_assert(channel_id > 0);
try_assert(block->datum.len >= count * (sizeof(MessageMeta) + MESSAGE_SIZE_MAX));
tsqlite_statement* st = q->messages.get_block;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_i64(st, "$channel_id", channel_id));
try_void(tsqlite_statement_bind_i64(st, "$first_message_id", first_message_id));
try_void(tsqlite_statement_bind_i64(st, "$count", count));
MessageBlock_reset(block);
str tmp_str = str_null;
while(true){
try(bool has_result, i, tsqlite_statement_step(st));
if(!has_result)
break;
// id
try(i64 message_id, i, tsqlite_statement_getResult_i64(st));
// sender_id
try(i64 sender_id, i, tsqlite_statement_getResult_i64(st));
// content
Array(u8) msg_content;
try_void(tsqlite_statement_getResult_blob(st, &msg_content));
// timestamp
try_void(tsqlite_statement_getResult_str(st, &tmp_str));
DateTime timestamp;
try_void(DateTime_parse(tmp_str.data, &timestamp));
MessageMeta msg_meta = MessageMeta_construct(
msg_content.len,
message_id,
sender_id,
timestamp);
try(bool write_success, u, MessageBlock_writeMessage(block, &msg_meta, msg_content));
try_assert(write_success == true);
}
Return RESULT_VOID;
}

50
src/server/db/User.c Normal file
View File

@@ -0,0 +1,50 @@
#include "server_db_internal.h"
Result(i64) User_findByUsername(ServerQueries* q, str username){
Deferral(4);
tsqlite_statement* st = q->users.find_by_username;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_str(st, "$username", username, NULL));
try(bool has_result, i, tsqlite_statement_step(st));
i64 user_id = 0;
if(has_result){
try(user_id, i, tsqlite_statement_getResult_i64(st));
try_assert(user_id > 0);
}
Return RESULT_VALUE(i, user_id);
}
Result(i64) User_register(ServerQueries* q, str username, Array(u8) token){
Deferral(4);
try_assert(username.len >= USERNAME_SIZE_MIN && username.len <= USERNAME_SIZE_MAX);
try_assert(token.len == PASSWORD_HASH_SIZE)
tsqlite_statement* st = q->users.insert;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_str(st, "$username", username, NULL));
try_void(tsqlite_statement_bind_blob(st, "$token", token, NULL));
try(bool has_result, i, tsqlite_statement_step(st));
try_assert(has_result);
try(i64 user_id, i, tsqlite_statement_getResult_i64(st));
try_assert(user_id > 0);
Return RESULT_VALUE(i, user_id);
}
Result(bool) User_tryAuthorize(ServerQueries* q, u64 id, Array(u8) token){
Deferral(4);
try_assert(token.len == PASSWORD_HASH_SIZE)
tsqlite_statement* st = q->users.compare_token;
Defer(tsqlite_statement_reset(st));
try_void(tsqlite_statement_bind_i64(st, "$id", id));
try_void(tsqlite_statement_bind_blob(st, "$token", token, NULL));
try(bool has_result, i, tsqlite_statement_step(st));
Return RESULT_VALUE(i, has_result);
}

153
src/server/db/server_db.c Normal file
View File

@@ -0,0 +1,153 @@
#include "server_db_internal.h"
#include "tlibc/filesystem.h"
Result(tsqlite_connection*) ServerDatabase_open(cstr file_path){
Deferral(64);
try_void(dir_createParent(file_path));
try(tsqlite_connection* db, p, tsqlite_connection_open(file_path));
bool success = false;
Defer(if(!success) tsqlite_connection_close(db));
///////////////////////////////////////////////////////////////////////////
// CHANNELS //
///////////////////////////////////////////////////////////////////////////
try(tsqlite_statement* create_table_channels, p, tsqlite_statement_compile(db, STR(
"CREATE TABLE IF NOT EXISTS channels (\n"
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
" name VARCHAR NOT NULL,\n"
" description VARCHAR NOT NULL\n"
");"
)));
Defer(tsqlite_statement_free(create_table_channels));
try_void(tsqlite_statement_step(create_table_channels));
///////////////////////////////////////////////////////////////////////////
// MESSAGES //
///////////////////////////////////////////////////////////////////////////
try(tsqlite_statement* create_table_messages, p, tsqlite_statement_compile(db, STR(
"CREATE TABLE IF NOT EXISTS messages (\n"
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
" channel_id INTEGER NOT NULL REFERENCES channels(id),\n"
" sender_id INTEGER NOT NULL REFERENCES users(id),\n"
" content BLOB NOT NULL,\n"
" timestamp DATETIME NOT NULL DEFAULT (\n"
" strftime('"MESSAGE_TIMESTAMP_FMT_SQL"', 'now', 'utc', 'subsecond')\n"
" )\n"
");"
)));
Defer(tsqlite_statement_free(create_table_messages));
try_void(tsqlite_statement_step(create_table_messages));
///////////////////////////////////////////////////////////////////////////
// USERS //
///////////////////////////////////////////////////////////////////////////
try(tsqlite_statement* create_table_users, p, tsqlite_statement_compile(db, STR(
"CREATE TABLE IF NOT EXISTS users (\n"
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
" username VARCHAR NOT NULL,\n"
" token BLOB NOT NULL,\n"
" registration_time DATETIME NOT NULL DEFAULT (\n"
" strftime('"MESSAGE_TIMESTAMP_FMT_SQL"', 'now', 'utc', 'subsecond')\n"
" )\n"
");"
)));
Defer(tsqlite_statement_free(create_table_users));
try_void(tsqlite_statement_step(create_table_users));
try(tsqlite_statement* create_index_username, p, tsqlite_statement_compile(db, STR(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username);"
)));
Defer(tsqlite_statement_free(create_index_username));
try_void(tsqlite_statement_step(create_index_username));
success = true;
Return RESULT_VALUE(p, db);
}
void ServerQueries_free(ServerQueries* q){
if(!q)
return;
tsqlite_statement_free(q->channels.insert);
tsqlite_statement_free(q->channels.update);
tsqlite_statement_free(q->channels.exists);
tsqlite_statement_free(q->messages.insert);
tsqlite_statement_free(q->messages.get_block);
tsqlite_statement_free(q->users.insert);
tsqlite_statement_free(q->users.find_by_username);
tsqlite_statement_free(q->users.compare_token);
free(q);
}
Result(ServerQueries*) ServerQueries_compile(tsqlite_connection* db){
Deferral(4);
ServerQueries* q = (ServerQueries*)malloc(sizeof(*q));
zeroStruct(q);
bool success = false;
Defer(if(!success) ServerQueries_free(q));
///////////////////////////////////////////////////////////////////////////
// CHANNELS //
///////////////////////////////////////////////////////////////////////////
try(q->channels.insert, p, tsqlite_statement_compile(db, STR(
"INSERT INTO\n"
"channels (id, name, description)\n"
"VALUES ($id, $name, $description);"
)));
try(q->channels.exists, p, tsqlite_statement_compile(db, STR(
"SELECT 1 FROM channels WHERE id = $id;"
)));
try(q->channels.update, p, tsqlite_statement_compile(db, STR(
"UPDATE channels\n"
"SET name = $name, description = $description\n"
"WHERE id = $id;"
)));
///////////////////////////////////////////////////////////////////////////
// MESSAGES //
///////////////////////////////////////////////////////////////////////////
try(q->messages.insert, p, tsqlite_statement_compile(db, STR(
"INSERT INTO\n"
"messages (channel_id, sender_id, content)\n"
"VALUES ($channel_id, $sender_id, $content)\n"
"RETURNING id, timestamp;"
)));
try(q->messages.get_block, p, tsqlite_statement_compile(db, STR(
"SELECT id, sender_id, content, timestamp FROM messages\n"
"WHERE id >= $first_message_id\n"
"AND channel_id = $channel_id\n"
"LIMIT $count;"
)));
///////////////////////////////////////////////////////////////////////////
// USERS //
///////////////////////////////////////////////////////////////////////////
try(q->users.insert, p, tsqlite_statement_compile(db, STR(
"INSERT INTO\n"
"users (username, token)\n"
"VALUES ($username, $token)\n"
"RETURNING id, registration_time;"
)));
try(q->users.find_by_username, p, tsqlite_statement_compile(db, STR(
"SELECT id FROM users WHERE username = $username;"
)));
try(q->users.compare_token, p, tsqlite_statement_compile(db, STR(
"SELECT 1 FROM users WHERE id = $id AND token = $token;"
)));
success = true;
Return RESULT_VALUE(p, q);
}

39
src/server/db/server_db.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include "tcp-chat.h"
#include "tsqlite.h"
#include "network/tcp-chat-protocol/v1.h"
/// @brief open DB and create tables
Result(tsqlite_connection*) ServerDatabase_open(cstr file_path);
typedef struct ServerQueries ServerQueries;
Result(ServerQueries*) ServerQueries_compile(tsqlite_connection* db);
void ServerQueries_free(ServerQueries* self);
Result(bool) Channel_exists(ServerQueries* q, i64 id);
/// @return true if new row was created
Result(bool) Channel_createOrUpdate(ServerQueries* q,
i64 id, str name, str description);
/// @return new message id
Result(i64) Channel_saveMessage(ServerQueries* q,
i64 channel_id, i64 sender_id, Array(u8) content,
DateTime* out_timestamp_utc);
/// @brief try to find count messages with id >= first_message_id
/// @param dst_block writes messages here. messages_count can be 0 if no messages were found
Result(void) Channel_loadMessageBlock(ServerQueries* q,
i64 channel_id, i64 first_message_id, u32 count,
MessageBlock* dst_block);
/// @return existing user id or 0
Result(i64) User_findByUsername(ServerQueries* q, str username);
/// @return new user id
Result(i64) User_register(ServerQueries* q, str username, Array(u8) token);
/// @return true for successful authorization
Result(bool) User_tryAuthorize(ServerQueries* q, u64 id, Array(u8) token);

View File

@@ -0,0 +1,27 @@
#pragma once
#include "server_db.h"
typedef struct ServerQueries {
struct {
/* ($id, $name, $description) -> void */
tsqlite_statement* insert;
/* ($id, $name, $description) -> void */
tsqlite_statement* update;
/* ($id) -> 1 or nothing */
tsqlite_statement* exists;
} channels;
struct {
/* ($channel_id, $sender_id, $content) -> (id, timestamp) */
tsqlite_statement* insert;
/* ($channel_id, $first_message_id, $count) -> [(id, sender_id, content, timestamp)] */
tsqlite_statement* get_block;
} messages;
struct {
/* ($username, $token) -> (id, registration_time) */
tsqlite_statement* insert;
/* ($username) -> (id) */
tsqlite_statement* find_by_username;
/* ($id, $token) -> 1 or nothing */
tsqlite_statement* compare_token;
} users;
} ServerQueries;

View File

@@ -21,27 +21,36 @@ declare_RequestHandler(GetMessageBlock)
Return RESULT_VOID;
}
// get message block from channel
Channel* ch = Server_tryGetChannel(conn->server, req.channel_id);
if(ch == NULL){
// validate messages_count
if(req.messages_count < 1 || req.messages_count > MESSAGE_BLOCK_COUNT_MAX){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("invalid message count in request") ));
Return RESULT_VOID;
}
// validate channel id
try(bool channel_exists, i, Channel_exists(conn->queries, req.channel_id));
if(!channel_exists){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("invalid channel id") ));
Return RESULT_VOID;
}
MessageBlockMeta meta;
Array(u8) block_data;
try_void(Channel_loadMessageBlock(ch, req.first_message_id, req.messages_count,
&meta, &block_data, true));
Defer(Array_u8_destroy(&block_data));
// get message block from channel
try_void(Channel_loadMessageBlock(conn->queries,
req.channel_id, req.first_message_id, req.messages_count,
&conn->message_block));
// send response
GetMessageBlockResponse res;
GetMessageBlockResponse_construct(&res, res_head,
meta.first_message_id, meta.messages_count, meta.data_size);
conn->message_block.messages_count, conn->message_block.offset);
try_void(EncryptedSocketTCP_sendStruct(&conn->sock, res_head));
try_void(EncryptedSocketTCP_sendStruct(&conn->sock, &res));
if(block_data.len != 0){
try_void(EncryptedSocketTCP_send(&conn->sock, block_data));
if(conn->message_block.offset != 0){
try_void(EncryptedSocketTCP_send(&conn->sock,
Array_u8_sliceTo(conn->message_block.datum, conn->message_block.offset))
);
}
Return RESULT_VOID;

View File

@@ -22,8 +22,8 @@ declare_RequestHandler(Login)
}
// validate username
str username_str = str_null;
str name_error_str = validateUsername_cstr(req.username, &username_str);
str username = str_null;
str name_error_str = validateUsername_cstr(req.username, &username);
if(name_error_str.data){
Defer(str_destroy(name_error_str));
try_void(sendErrorMessage(log_ctx, conn, res_head,
@@ -31,42 +31,28 @@ declare_RequestHandler(Login)
Return RESULT_VOID;
}
// lock users cache
idb_lockTable(srv->users.table);
bool unlocked_users_cache_mutex = false;
Defer(
if(!unlocked_users_cache_mutex)
idb_unlockTable(srv->users.table)
);
// try get id from name cache
u64* id_ptr = HashMap_tryGetPtr(&srv->users.name_id_map, username_str);
if(id_ptr == NULL){
// get user by id
try(u64 user_id, i, User_findByUsername(conn->queries, username));
if(user_id == 0){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("Username is not registered") ));
Return RESULT_VOID;
}
u64 user_id = *id_ptr;
// get user by id
try_assert(user_id < srv->users.list.len);
UserInfo* u = srv->users.list.data + user_id;
// TODO: get user token
Array(u8) token = Array_u8_construct(req.token, sizeof(req.token));
try(bool authorized, i, User_tryAuthorize(conn->queries, user_id, token));
// validate token hash
if(memcmp(req.token, u->token, sizeof(req.token)) != 0){
if(!authorized){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("wrong password") ));
Return RESULT_VOID;
}
// manually unlock mutex
idb_unlockTable(srv->users.table);
unlocked_users_cache_mutex = true;
// authorize
conn->authorized = true;
conn->user_id = user_id;
logInfo("authorized user '%s'", username_str.data);
logInfo("authorized user '%s' with id "FMT_i64, username.data, user_id);
// send response
LoginResponse res;

View File

@@ -22,8 +22,8 @@ declare_RequestHandler(Register)
}
// validate username
str username_str = str_null;
str name_error_str = validateUsername_cstr(req.username, &username_str);
str username = str_null;
str name_error_str = validateUsername_cstr(req.username, &username);
if(name_error_str.data){
Defer(str_destroy(name_error_str));
try_void(sendErrorMessage(log_ctx, conn, res_head,
@@ -31,46 +31,19 @@ declare_RequestHandler(Register)
Return RESULT_VOID;
}
// lock users cache
idb_lockTable(srv->users.table);
bool unlocked_users_cache_mutex = false;
// unlock mutex on error catch
Defer(
if(!unlocked_users_cache_mutex)
idb_unlockTable(srv->users.table)
);
// check if name is taken
if(HashMap_tryGetPtr(&srv->users.name_id_map, username_str) != NULL){
try(u64 user_id, i, User_findByUsername(conn->queries, username));
if(user_id != 0){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("Username already exists") ));
LogSeverity_Warn, STR("Username is already taken") ));
Return RESULT_VOID;
}
// initialize new user
UserInfo user;
zeroStruct(&user);
user.name_len = username_str.len;
memcpy(user.name, username_str.data, user.name_len);
memcpy(user.token, req.token, sizeof(req.token));
DateTime_getUTC(&user.registration_time_utc);
// save new user to db and cache
try(u64 user_id, u,
idb_pushRow(srv->users.table, &user, false)
);
try_assert(user_id == srv->users.list.len);
List_UserInfo_pushMany(&srv->users.list, &user, 1);
try_assert(HashMap_tryPush(&srv->users.name_id_map, username_str, &user_id));
// manually unlock mutex
idb_unlockTable(srv->users.table);
unlocked_users_cache_mutex = true;
logInfo("registered user '%s'", username_str.data);
// register new user
Array(u8) token = Array_u8_construct(req.token, sizeof(req.token));
try(user_id, i, User_register(conn->queries, username, token));
logInfo("registered user '"FMT_str"' with id "FMT_i64,
str_unwrap(username), user_id);
// send response
RegisterResponse res;

View File

@@ -21,6 +21,7 @@ declare_RequestHandler(SendMessage)
Return RESULT_VOID;
}
// validate content size
if(req.data_size < MESSAGE_SIZE_MIN || req.data_size > MESSAGE_SIZE_MAX){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("invalid message size") ));
@@ -29,24 +30,26 @@ declare_RequestHandler(SendMessage)
}
// receive message data
Array(u8) message_data = Array_u8_alloc(req.data_size);
try_void(EncryptedSocketTCP_recv(&conn->sock, message_data, SocketRecvFlag_WholeBuffer));
try_void(EncryptedSocketTCP_recv(&conn->sock, conn->message_content, SocketRecvFlag_WholeBuffer));
// save message to channel
Channel* ch = Server_tryGetChannel(conn->server, req.channel_id);
if(ch == NULL){
// validate channel id
try(bool channel_exists, i, Channel_exists(conn->queries, req.channel_id));
if(!channel_exists){
try_void(sendErrorMessage(log_ctx, conn, res_head,
LogSeverity_Warn, STR("invalid channel id") ));
Return RESULT_VOID;
}
MessageMeta message_meta;
try_void(Channel_saveMessage(ch, message_data, conn->user_id,
&message_meta, true));
// save message to channel
DateTime timestamp;
try(i64 message_id, i, Channel_saveMessage(conn->queries,
req.channel_id, conn->user_id, conn->message_content,
&timestamp));
// send response
SendMessageResponse res;
SendMessageResponse_construct(&res, res_head,
message_meta.id, message_meta.receiving_time_utc);
message_id, timestamp);
try_void(EncryptedSocketTCP_sendStruct(&conn->sock, res_head));
try_void(EncryptedSocketTCP_sendStruct(&conn->sock, &res));

View File

@@ -1,8 +1,6 @@
#pragma once
#include "network/tcp-chat-protocol/v1.h"
#include "server/server_internal.h"
Result(void) sendErrorMessage(
cstr log_ctx, ClientConnection* conn, PacketHeader* res_head,
LogSeverity log_severity, str msg);

View File

@@ -9,7 +9,7 @@ Result(void) sendErrorMessage(
cstr log_ctx, ClientConnection* conn, PacketHeader* res_head,
LogSeverity log_severity, str msg)
{
Deferral(1);
Deferral(4);
//limit ErrorMessage size to fit into EncryptedSocketTCP.internal_buffer_size
if(msg.len > ERROR_MESSAGE_MAX_SIZE)
@@ -44,7 +44,7 @@ Result(void) sendErrorMessage_f(
ClientConnection* conn, PacketHeader* res_head,
LogSeverity log_severity, cstr format, ...)
{
Deferral(1);
Deferral(4);
va_list argv;
va_start(argv, format);

View File

@@ -1,10 +1,6 @@
#include <pthread.h>
#include "tlibc/filesystem.h"
#include "tlibc/time.h"
#include "tlibc/base64.h"
#include "tlibc/algorithms.h"
#include "server/server_internal.h"
#include "network/tcp-chat-protocol/v1.h"
#include "server/responses/responses.h"
#include "tlibtoml.h"
@@ -20,17 +16,9 @@ void Server_free(Server* self){
RSA_destroyPrivateKey(&self->rsa_sk);
RSA_destroyPublicKey(&self->rsa_pk);
idb_close(self->db);
List_UserInfo_destroy(&self->users.list);
HashMap_destroy(&self->users.name_id_map);
for(u32 i = 0; i < self->channels.list.len; i++){
Channel_free(self->channels.list.data[i]);
}
List_ChannelPtr_destroy(&self->channels.list);
HashMap_destroy(&self->channels.name_id_map);
free(self->db_path);
ServerQueries_free(self->queries);
tsqlite_connection_close(self->db);
free(self);
}
@@ -58,78 +46,51 @@ Result(Server*) Server_create(str config_file_content, cstr config_file_name,
try(TomlTable* config_top, p, toml_load_str_filename(config_file_content, config_file_name));
Defer(TomlTable_free(config_top));
// [server]
try(TomlTable* config_server, p, TomlTable_get_table(config_top, STR("server")))
// parse name
// name
try(str* v_name, p, TomlTable_get_str(config_server, STR("name")));
self->name = str_copy(*v_name);
// parse description
// description
try(str* v_desc, p, TomlTable_get_str(config_server, STR("description")));
self->description = str_copy(*v_desc);
// parse local_address
// local_address
try(str* v_local_address, p, TomlTable_get_str(config_server, STR("local_address")));
try_assert(v_local_address->isZeroTerminated);
try_void(EndpointIPv4_parse(v_local_address->data, &self->local_end));
// parse landing_channel_id
// landing_channel_id
try(i64 v_landing_channel_id, i, TomlTable_get_integer(config_server, STR("landing_channel_id")));
self->landing_channel_id = v_landing_channel_id;
// [keys]
try(TomlTable* config_keys, p, TomlTable_get_table(config_top, STR("keys")))
// parse rsa_private_key
// rsa_private_key
try(str* v_rsa_sk, p, TomlTable_get_str(config_keys, STR("rsa_private_key")));
try_assert(v_rsa_sk->isZeroTerminated);
try_void(RSA_parsePrivateKey_base64(v_rsa_sk->data, &self->rsa_sk));
// parse rsa_public_key
// rsa_public_key
try(str* v_rsa_pk, p, TomlTable_get_str(config_keys, STR("rsa_public_key")));
try_assert(v_rsa_pk->isZeroTerminated);
try_void(RSA_parsePublicKey_base64(v_rsa_pk->data, &self->rsa_pk));
// [db]
try(TomlTable* config_db, p, TomlTable_get_table(config_top, STR("database")))
// parse db_aes_key
try(str* v_db_aes_key, p, TomlTable_get_str(config_db, STR("aes_key")));
str db_aes_key_s = *v_db_aes_key;
Array(u8) db_aes_key = Array_u8_alloc(base64_decodedSize(db_aes_key_s.data, db_aes_key_s.len));
Defer(free(db_aes_key.data));
base64_decode(db_aes_key_s.data, db_aes_key_s.len, db_aes_key.data);
// parse db_dir
try(str* v_db_dir, p, TomlTable_get_str(config_db, STR("dir")));
// path
try(str* v_db_path, p, TomlTable_get_str(config_db, STR("path")));
self->db_path = str_copy(*v_db_path).data;
// open DB
try(self->db, p, idb_open(*v_db_dir, db_aes_key));
logInfo("loading database '%s'", self->db_path);
try(self->db, p, ServerDatabase_open(self->db_path));
try(self->queries, p, ServerQueries_compile(self->db));
// build users cache
logInfo("loading users...");
try(self->users.table, p,
idb_getOrCreateTable(self->db, str_null, STR("users"), sizeof(UserInfo), false)
);
// load whole users table to list
try_void(
idb_createListFromTable(self->users.table, (void*)&self->users.list, false)
);
// build name-id map
try(u64 users_count, u, idb_getRowCount(self->users.table, false));
HashMap_construct(&self->users.name_id_map, u64, NULL);
for(u64 id = 0; id < users_count; id++){
UserInfo* user = self->users.list.data + id;
str key = str_construct(user->name, user->name_len, true);
if(!HashMap_tryPush(&self->users.name_id_map, key, &id)){
Return RESULT_ERROR_FMT("duplicate user name '"FMT_str"'", str_expand(key));
}
}
logInfo("loaded "FMT_u64" users", users_count);
// parse channels
// [channels]
logDebug("loading channels...");
HashMap_construct(&self->channels.name_id_map, u64, NULL);
self->channels.list = List_ChannelPtr_alloc(32);
try(TomlTable* config_channels, p, TomlTable_get_table(config_top, STR("channels")));
HashMapIter channels_iter = HashMapIter_create(config_channels);
while(HashMapIter_moveNext(&channels_iter)){
HashMapKeyValue kv;
@@ -140,26 +101,15 @@ Result(Server*) Server_create(str config_file_content, cstr config_file_name,
if(val->type != TLIBTOML_TABLE)
continue;
logInfo("loading channel '"FMT_str"'", str_expand(name))
logInfo("loading channel '"FMT_str"'", str_unwrap(name))
TomlTable* config_channel = val->table;
try(u64 id, u, TomlTable_get_integer(config_channel, STR("id")));
try(i64 id, u, TomlTable_get_integer(config_channel, STR("id")));
try(str* v_ch_desc, p, TomlTable_get_str(config_channel, STR("description")))
str description = *v_ch_desc;
if(!HashMap_tryPush(&self->channels.name_id_map, name, &id)){
Return RESULT_ERROR_FMT("duplicate channel '"FMT_str"'", str_expand(name));
try_void(Channel_createOrUpdate(self->queries, id, name, description));
}
logDebug("loading messages...");
try(Channel* channel, p, Channel_create(id, name, description, self->db, false));
logDebug("loaded "FMT_u64" messages", channel->messages.count);
List_ChannelPtr_push(&self->channels.list, channel);
}
logDebug("loaded "FMT_u32" channels", self->channels.list.len);
// sort channels list by id
for(u32 i = 0; i < self->channels.list.len; i++);
insertionSort_inline(self->channels.list.data, self->channels.list.len, ->id);
success = true;
Return RESULT_VALUE(p, self);
}
@@ -184,7 +134,7 @@ Result(void) Server_run(Server* server){
Defer(free(local_end_str.data));
logInfo("server is listening at %s", local_end_str.data);
u64 session_id = 1;
i64 session_id = 1;
while(true){
ConnectionHandlerArgs* args = (ConnectionHandlerArgs*)malloc(sizeof(ConnectionHandlerArgs));
args->server = server;
@@ -201,16 +151,6 @@ Result(void) Server_run(Server* server){
Return RESULT_VOID;
}
Channel* Server_tryGetChannel(Server* self, u64 id){
i32 index;
binarySearch_inline(self->channels.list.data, self->channels.list.len, id, ->id, index);
if(index == -1){
return NULL;
}
Channel* channel = self->channels.list.data[index];
return channel;
}
static void* handleConnection(void* _args){
ConnectionHandlerArgs* args = (ConnectionHandlerArgs*)_args;
Server* server = args->server;

View File

@@ -1,56 +1,13 @@
#pragma once
#include "tlibc/collections/HashMap.h"
#include "tlibc/collections/List.h"
#include "tlibc/collections/LList.h"
#include "tcp-chat/tcp-chat.h"
#include "tcp-chat/server.h"
#include "tcp-chat.h"
#include "cryptography/AES.h"
#include "cryptography/RSA.h"
#include "network/encrypted_sockets.h"
#include "db/idb.h"
#include "db/tables.h"
#include "network/tcp-chat-protocol/v1.h"
#include "db/server_db.h"
typedef struct ClientConnection ClientConnection;
List_declare(UserInfo);
List_declare(MessageBlockMeta);
LList_declare(MessageBlock);
#define MESSAGE_BLOCKS_CACHE_COUNT 50
typedef struct Channel {
u64 id;
str name;
str description;
struct {
u64 count;
Table* blocks_table;
Table* blocks_meta_table;
List(MessageBlockMeta) blocks_meta_list; // index is id
// last MESSAGE_BLOCKS_CACHE_COUNT MessageBlocks, ascending
// new messages are written to the last block
LList(MessageBlock) blocks_queue;
} messages;
} Channel;
typedef Channel* ChannelPtr;
List_declare(ChannelPtr);
Result(Channel*) Channel_create(u64 id, str name, str description,
IncrementalDB* db, bool lock_db);
void Channel_free(Channel* self);
Result(void) Channel_saveMessage(Channel* self, Array(u8) message_data, u64 sender_id,
MessageMeta* out_message_meta, bool lock_tables);
/// @brief try to find `count` messages starting from `fisrt_message_id`
/// @param out_meta information about found messages, .count can be 0 if no messages found
/// @param out_block allocates buffer on heap and copies them there, .len can be 0 if no messages found
Result(void) Channel_loadMessageBlock(Channel* self, u64 fisrt_message_id, u32 count,
MessageBlockMeta* out_meta, NULLABLE(Array(u8)*) out_block, bool lock_tables);
typedef struct Server {
/* from constructor */
void* logger;
@@ -59,42 +16,41 @@ typedef struct Server {
/* from config */
str name;
str description;
u64 landing_channel_id;
i64 landing_channel_id;
EndpointIPv4 local_end;
br_rsa_private_key rsa_sk;
br_rsa_public_key rsa_pk;
/* database and cache*/
IncrementalDB* db;
struct {
Table* table;
List(UserInfo) list; // index is id
HashMap(u64) name_id_map;
} users;
struct {
List(ChannelPtr) list;
HashMap(u64) name_id_map;
} channels;
char* db_path;
tsqlite_connection* db;
ServerQueries* queries; /* for server listener thread only */
} Server;
Channel* Server_tryGetChannel(Server* self, u64 id);
typedef struct ClientConnection {
Server* server;
u64 session_id;
i64 session_id;
EndpointIPv4 client_end;
Array(u8) session_key;
EncryptedSocketTCP sock;
bool authorized;
u64 user_id; // -1 for unauthorized
i64 user_id; // 0 for unauthorized
/* buffers */
MessageBlock message_block; // requested message block
Array(u8) message_content; // sent message
/* database */
tsqlite_connection* db;
ServerQueries* queries;
} ClientConnection;
typedef struct ConnectionHandlerArgs {
Server* server;
Socket accepted_socket_tcp;
EndpointIPv4 client_end;
u64 session_id;
i64 session_id;
} ConnectionHandlerArgs;
Result(ClientConnection*) ClientConnection_accept(ConnectionHandlerArgs* args);

View File

@@ -5,16 +5,17 @@ description = """\
Qqqqq...\
"""
local_address = '127.0.0.1:9988'
landing_channel_id = 0
landing_channel_id = 1
# do not create channels with the same id
[channels.general]
id = 0
id = 1
description = "a text channel"
[database]
dir = 'server-db'
aes_key = '<generate with './tcp-chat --random-bytes-base64 32'>'
path = 'tcp-chat-server/server.sqlite'
# on windows use backslashes
# path = 'tcp-chat-server\server.sqlite'
[keys]
rsa_private_key = '<generate with './tcp-chat --rsa-gen-random'>'