This repository has been archived on 2021-12-16. You can view files and clone it, but cannot push or open issues or pull requests.
lemonbbs/src/main.c
2021-12-16 22:16:13 +01:00

363 lines
16 KiB
C

#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <toml.h>
#include "settings.h"
#include "ansicolor.h"
#include "connlist.h"
#include "utils.h"
#include "help.h"
#include "dbutil.h"
#include "chat.h"
// config values
unsigned short configPort;
unsigned int configConnlimit;
char *configCertfile, *configKeyfile, *configPassword, sslInUse;
// return value: 0 = succeeded ; 1 = failed
uint8_t initConfigFromFile(const char* configPath) {
FILE* configFile = fopen(configPath, "r");
if (configFile == NULL) {logMessage(error, "Config file does not exist."); return 1;}
char errbuf[256];
toml_table_t* conf = toml_parse_file(configFile, errbuf, sizeof(errbuf));
fclose(configFile);
if (!conf) {logMessage(error, "Config parsing error: %s", errbuf); return 1;}
toml_table_t* server = toml_table_in(conf, "listen");
if (!server) {logMessage(error, "Config parsing error: missing [listen]"); return 1;}
toml_datum_t confPort = toml_int_in(server, "port");
if (!confPort.ok) {logMessage(error, "Config parsing error: missing 'port' field"); return 1;}
if (confPort.u.b > USHRT_MAX) {logMessage(error, "Config parsing error: port out of range"); return 1;}
configPort = confPort.u.b;
toml_table_t* ssl = toml_table_in(conf, "ssl");
if (ssl) {
toml_datum_t confCertfile = toml_string_in(ssl, "certfile");
if (!confCertfile.ok) {logMessage(error, "Config parsing error: missing 'certfile' field"); return 1;}
configCertfile = confCertfile.u.s;
toml_datum_t confKeyfile = toml_string_in(ssl, "keyfile");
if (!confKeyfile.ok) {logMessage(error, "Config parsing error: missing 'keyfile' field"); return 1;}
configKeyfile = confKeyfile.u.s;
toml_datum_t confPassword = toml_string_in(ssl, "password");
if (!confPassword.ok) {logMessage(error, "Config parsing error: missing 'password' field"); return 1;}
configPassword = confPassword.u.s;
}
toml_table_t* bbs = toml_table_in(conf, "bbs");
if (!bbs) {logMessage(error, "Config parsing error: missing [bbs]"); return 1;}
toml_array_t* confChannels = toml_array_in(bbs, "channelList");
if (!confChannels) {logMessage(error, "Config parsing error: missing 'channelList' field"); return 1;}
unsigned int channelCount = toml_array_nelem(confChannels);
if (channelCount == 0) {logMessage(error, "Config parsing error: empty 'channelList' field"); return 1;}
toml_datum_t confConnlimit = toml_int_in(bbs, "connlimit");
if (!confConnlimit.ok) {logMessage(error, "Config parsing error: missing 'connlimit' field"); return 1;}
configConnlimit = confConnlimit.u.b;
initChat(channelCount);
for (int i = 0; i < channelCount; ++i) {
toml_datum_t channelName = toml_string_at(confChannels, i);
if (!channelName.ok) {logMessage(error, "Config parsing error: invalid 'channelList' field"); return 1;}
char* channelNameStr = channelName.u.s;
trimWhitespace(channelNameStr);
unsigned int newLength = sanitizeString(channelNameStr, strlen(channelNameStr));
if (newLength == 0) {
logMessage(error, "Config parsing error: 'channelList' field contains an empty entry");
return 1;
}
if (newLength >= CHANNEL_NAME_LEN) {
logMessage(error, "Config parsing error: 'channelList' field contains an entry that is longer than "STR(CHANNEL_NAME_LEN)" characters.");
return 1;
}
setChannelName(i, channelNameStr);
free(channelNameStr);
}
toml_free(conf);
return 0;
}
int askCredentials(struct LemonClientSocket *socket, char* username, char* password, int bufLength) {
while (1) {
lolib_writeSocket(socket, METASTRING(ANSI_YELLOW"\n Username: "ANSI_RESET));
int read = lolib_readSocket(socket, username, bufLength-1);
if (read < 1) return -1; // IO error
if (username[0] == 'q') return 0; // cancelled
username[read] = 0; // null-terminator
trimWhitespace(username);
if (sanitizeString(username, read) == 0) {
lolib_writeSocket(socket, METASTRING(ANSI_RED"\n Error: Username empty."));
continue;
}
lolib_writeSocket(socket, METASTRING(ANSI_YELLOW" Password: "ANSI_RESET));
read = lolib_readSocket(socket, password, bufLength-1);
if (read < 1) return -1; // IO error
password[read] = 0; // null-terminator
trimWhitespace(password);
if (sanitizeString(password, read) == 0) {
lolib_writeSocket(socket, METASTRING(ANSI_RED"\n Error: Password empty."));
continue;
}
return 1;
}
}
int channelSelector(struct LemonClientSocket *socket) {
lolib_writeSocket(socket, METASTRING(ANSI_RED"\n +--------+\n | "ANSI_GREEN"Chat"ANSI_RED" |\n +--------+\n"ANSI_YELLOW" WARNING: Chat feature is EXPERIMENTAL and may not work correctly.\n\n"));
for (size_t i = 0; i < getChannelCount(); ++i) {
char line[128];
int toWrite = snprintf(line, 128, ANSI_RED" %lu. "ANSI_GREEN"#%s "ANSI_CYAN"[%d/%d online]\n", i + 1, getChannelName(i), countUsers(i), USERS_PER_CHANNEL);
lolib_writeSocket(socket, line, toWrite);
}
while (1) {
char answer[8];
lolib_writeSocket(socket, METASTRING(ANSI_YELLOW"\n Pick a channel ['q' to cancel]: "ANSI_RESET));
int read = lolib_readSocket(socket, answer, 7);
if (read < 1) return -1; // IO error
if (answer[0] == 'q') return -2; // cancelled
if (sanitizeString(answer, read) == 0) continue;
int channelSelected = strtol(answer, NULL, 10);
if (!inRange(channelSelected, 1, getChannelCount())) {
lolib_writeSocket(socket, METASTRING(ANSI_YELLOW" No channel with given ID exists."));
} else {
return channelSelected-1;
}
}
}
int chatWindow(struct Connection* cta, size_t channelId) {
char inbuf[400], outbuf[500];
while (1) {
int read = lolib_readSocket(&cta->socket, inbuf, sizeof(inbuf)-1);
if (read < 1) return -1; // IO error
inbuf[read] = 0;
trimWhitespace(inbuf);
if (sanitizeString(inbuf, read) == 0) {
continue;
}
if (inbuf[0] == '/') {
if (strcmp(inbuf+1, "help") == 0) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_CYAN"Available commands:\n = /help - displays this help screen\n = /quit - leaves the text channel\n = /info - info about the text channel\n"ANSI_RESET));
}
else if (strcmp(inbuf+1, "info") == 0) {
int toWrite = snprintf(outbuf, sizeof(outbuf), ANSI_GREEN"#%s "ANSI_CYAN"[%d/%d users]\n"ANSI_RESET, getChannelName(channelId), countUsers(channelId), USERS_PER_CHANNEL);
lolib_writeSocket(&cta->socket, outbuf, toWrite);
}
else if (strcmp(inbuf+1, "quit") == 0) {
return 0;
}
} else {
int toWrite = snprintf(outbuf, sizeof(outbuf), ANSI_YELLOW" <%s>:"ANSI_RESET" %s\n", cta->username, inbuf);
broadcastString(channelId, outbuf, toWrite);
}
}
}
void* connectionHandler(struct Connection* cta) {
{
char writebuf[256];
time_t timeNow = time(0);
int toWrite = snprintf(writebuf, sizeof(writebuf), ANSI_RED"\n +----------+ +--------------------------+\n | "ANSI_GREEN"LemonBBS"ANSI_RED" |-|"ANSI_GREEN" %.24s "ANSI_RED"|\n +----------+ +--------------------------+\n"ANSI_CYAN" * Made by ./lemon.sh\n * Recommended text mode: 80x25\n", ctime(&timeNow));
lolib_writeSocket(&cta->socket, writebuf, toWrite);
}
cta->userId = -1;
while (1) {
char readbuf[256];
loopStart:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_YELLOW"\n > "ANSI_RESET));
int sslRead = lolib_readSocket(&cta->socket, readbuf, sizeof(readbuf)-1);
if (sslRead < 1) goto end;
readbuf[sslRead] = 0;
trimWhitespace(readbuf);
if (strcmp(readbuf, "chat") == 0) {
if (cta->userId == -1) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED" You're not logged in!"));
goto loopStart;
}
int32_t channelSelected = channelSelector(&cta->socket);
switch (channelSelected) {
case -1:
goto end;
case -2:
goto loopStart;
default:
break;
}
if (isUserInChat(channelSelected, cta->userId)) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED" You're already in this channel on a different session."));
goto loopStart;
}
if (addChatUser(channelSelected, cta) == 0) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_YELLOW" This chat room is full. Try again later."));
goto loopStart;
}
char lvenMessage[128];
int toWrite = snprintf(lvenMessage, sizeof(lvenMessage), "\n %s joined the chat.\n", cta->username);
broadcastString(channelSelected, lvenMessage, toWrite);
int chatWindowResult = chatWindow(cta, channelSelected);
removeChatUser(channelSelected, cta->userId);
toWrite = snprintf(lvenMessage, sizeof(lvenMessage), "\n %s left the chat.\n", cta->username);
broadcastString(channelSelected, lvenMessage, toWrite);
if (chatWindowResult == -1) goto end;
} else if (strcmp(readbuf, "register") == 0) {
if (cta->userId != -1) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_GREEN" You're already logged in!"));
goto loopStart;
}
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n +---------------+\n | "ANSI_GREEN"New Account"ANSI_RED" |\n +---------------+\n \n "ANSI_CYAN"Welcome to LemonBBS!\n To register, type your desired username and password below. ['q' to cancel].\n"));
char username[64], password[64];
while (1) {
switch (askCredentials(&cta->socket, username, password, 64)) {
case -1:
goto end;
case 0:
goto loopStart;
}
switch (dbUsernameTaken(username)) {
case -1:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n An error has occurred while processing your request. Please try again later."));
goto loopStart;
case 1:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n Error: Username already taken."));
continue;
}
char writebuf[256];
int toWrite = snprintf(writebuf, 256, ANSI_CYAN"\n Is this correct?\n "ANSI_RESET"Username: %s\n Password: %s\n "ANSI_YELLOW"[y/n] > ", username, password);
lolib_writeSocket(&cta->socket, writebuf, toWrite);
char answer[8];
if (lolib_readSocket(&cta->socket, answer, 8) < 1) goto end;
if (answer[0] != 'y') continue;
switch (dbInsertUser(username, password)) {
case -1:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n An error has occurred while processing your request. Please try again later."));
break;
case 1:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_GREEN"\n Account successfully created! Use 'login' to log in."));
}
goto loopStart;
}
} else if (strcmp(readbuf, "login") == 0) {
if (cta->userId != -1) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_YELLOW" You're already logged in!"));
goto loopStart;
}
char username[64], password[64];
switch (askCredentials(&cta->socket, username, password, 64)) {
case -1:
goto end;
case 0:
goto loopStart;
}
int64_t verifyResult = dbVerifyLogin(username, password);
switch (verifyResult) {
case -1:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n An error has occurred while processing your request. Please try again later.")); break;
case -2:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n No account with this username exists. Create a new account using 'register'.")); break;
case -3:
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED"\n Invalid password.")); break;
default:
strcpy(cta->username, username);
cta->userId = verifyResult;
lolib_writeSocket(&cta->socket, METASTRING(ANSI_GREEN"\n Logged in successfully!"));
}
} else if (strcmp(readbuf, "logout") == 0) {
if (cta->userId == -1) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_YELLOW" You're not logged in!"));
goto loopStart;
}
cta->userId = -1;
lolib_writeSocket(&cta->socket, METASTRING(ANSI_GREEN" You have been logged out."));
} else if (strcmp(readbuf, "help") == 0) {
lolib_writeSocket(&cta->socket, help_txt, sizeof(help_txt));
} else if (strcmp(readbuf, "mail") == 0) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED" Error: LemonMail service is not available right now."));
} else if (strcmp(readbuf, "disconnect") == 0) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RESET"Thanks for using LemonBBS!\n"));
goto end;
} else if (strcmp(readbuf, "account") == 0) {
if (cta->userId == -1) {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED" You're not logged in!"));
goto loopStart;
}
char outbuf[256];
int toWrite = snprintf(outbuf, sizeof(outbuf), " User ID: %ld | Username: %s", cta->userId, cta->username);
lolib_writeSocket(&cta->socket, outbuf, toWrite);
} else {
lolib_writeSocket(&cta->socket, METASTRING(ANSI_RED" Unknown command!"));
}
}
end:
lolib_finalize(&cta->socket);
deallocateConnection(cta);
return NULL;
}
#ifdef _WIN32
void enableVT() {
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE) return;
DWORD dwMode = 0;
if (!GetConsoleMode(hOut, &dwMode)) return;
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
#endif
int main(/*int argc, char *argv[]*/)
{
#ifdef unix
signal(SIGPIPE, SIG_IGN);
#endif
#ifdef _WIN32
enableVT();
#endif
lolib_init();
printf(ANSI_CYAN "\n LemonBBS Server v0.3a\n -----------------------\n"ANSI_RESET);
logMessage(debug, "Initializing...");
if (initConfigFromFile(CONFIG_PATH) == 1) exit(1);
switch (dbInit()) {
case -1:
logMessage(error, "Couldn't open file '"DB_PATH"'.");
exit(1);
case -2:
logMessage(error, "Couldn't initialize the database.");
exit(1);
}
initConnList(configConnlimit);
LemonServerSocket serverSocket;
if (lolib_createServerSocket("127.0.0.1", 3333, &serverSocket) != 0) {
logMessage(error, "Couldn't initialize the socket.");
exit(1);
}
logMessage(debug, "Done! Listening on "ANSI_BLUE"%d"ANSI_RESET".", configPort);
while (1) {
struct LemonClientSocket connSock;
if (lolib_acceptTcpSocket(serverSocket, &connSock)) {
logMessage(warning, "A client failed to connect.");
continue;
}
struct Connection* newConnection = allocateConnection();
if (newConnection == NULL) {
lolib_writeSocket(&connSock, METASTRING("Sorry, all slots are currently in use. Try again later.\n"));
lolib_finalize(&connSock);
continue;
}
newConnection->socket = connSock;
pthread_t connThread;
pthread_create(&connThread, NULL, (void *(*)(void *)) connectionHandler, newConnection);
pthread_detach(connThread);
}
}