SilverMUD/source/server/main.c

274 lines
9.5 KiB
C
Raw Normal View History

Version 0.0.1 of the rewritten client and server. - Server is capable of accepting an arbitrary number of TLS encrypted client connections. - Server relays messages from one client to the others. - Server spawns a Scheme REPL available over a Unix socket. - Client is a two-pane ncurses-based client, with an input area, chat log, and game status log. - Temporary NAME command exists to change names of players. Squashed commit of the following: commit 442a9319e82e49d6040b5a3015271a951edb9375 Author: Barry Kane <barra@ocathain.ie> Date: Mon Oct 30 16:57:15 2023 +0000 Removed unneeded check, added temporary name command commit a66a07c897cf37b5624bcfa87c309fbbf602ea61 Author: Barry Kane <barra@ocathain.ie> Date: Sun Oct 29 20:15:41 2023 +0000 Properly remove disconnected players commit 81fc72a1d7d55893428c6202be8bd49cbc570ea9 Author: Barry Kane <barra@ocathain.ie> Date: Sun Oct 29 17:20:14 2023 +0000 Added system messages to client and added welcome message from server. commit a1b1b8044989c1d8e60492c2a3054d8885de562c Author: Barra Ó Catháin <barra@ocathain.ie> Date: Tue Sep 12 23:25:44 2023 +0100 Removed now unneeded placeholders, added stubs for "rulebooks" in documentation. commit 54b613befe86637db4aa69adec9e805d4eb355a2 Author: Barra Ó Catháin <barra@ocathain.ie> Date: Tue Sep 12 22:32:19 2023 +0100 Basic implementation of player lists and tying connections to players commit 5a53e9f1974598c62602b3b812398884906b8d00 Author: Barra Ó Catháin <barra@ocathain.ie> Date: Sun Sep 10 17:24:46 2023 +0100 Added basic player type containing a name, and made the server echo messages with player name. commit 3fc75ef30fb7668b9d6d3e7b777aeac4bd9d844a Author: Barry Kane <barra@ocathain.ie> Date: Thu Aug 31 01:44:17 2023 +0100 Basic message receiver, server now echoes messages to all clients. commit b292966588327bef59a102cf7b92dc88f8aed7b3 Author: Barry Kane <barra@ocathain.ie> Date: Mon Aug 28 02:53:31 2023 +0100 Fixed window height calculations. commit 50dcddfc56710964bb4c6a7f3315e20b23ca8200 Author: Barry Kane <barra@ocathain.ie> Date: Mon Aug 28 02:29:21 2023 +0100 Initial ncurses setup, and layout of client. commit c043da64a20064841dc768e85e6735b3b5a731fb Author: Barry Kane <barra@ocathain.ie> Date: Sat Aug 26 00:48:28 2023 +0100 Modify server and client to begin using ClientToServer messages. commit 0104a11a7ecb55f0a56f8b012b63a7a005d03f18 Author: Barry Kane <barra@ocathain.ie> Date: Fri Aug 25 00:34:05 2023 +0100 Added basic client capable of connecting to the server. commit 080e46fe994dc0a1e170e7d9b681ff4679f12e06 Author: Barry Kane <barra@ocathain.ie> Date: Thu Aug 24 00:12:27 2023 +0100 Set up GNU Autotools as build system. commit 0814e437cdc0c00fe3474d86dc8bf7ccd8f1d607 Author: Barry Kane <barra@ocathain.ie> Date: Tue Aug 22 02:02:29 2023 +0100 Basic connection handling (using previous version of client) commit 9801be3622646aa1639884e07459cbd1cdb3a17e Author: Barry Kane <barra@ocathain.ie> Date: Sat Aug 19 16:00:57 2023 +0100 Renamed src back to source, because I liked it better commit e2ef744e87c5652f144ee47ba878038f5097cbea Author: Barry Kane <barra@ocathain.ie> Date: Sat Aug 19 00:18:03 2023 +0100 Moved scheme initialization to main thread, added basic networking The server can now listen on a port and send data to a client. commit 8b0920c35dde3ad766bfc17528ce83e5891a4f1b Author: Barry Kane <barra@ocathain.ie> Date: Fri Aug 18 00:45:24 2023 +0100 Added basic implemantation of message structures. commit 6ed532c3688360ba6aed5abfb83392ce7b000216 Author: Barry Kane <barra@ocathain.ie> Date: Thu Aug 17 00:21:20 2023 +0100 Added structure section. commit 48f0858735f5f5aaf45e23d61a71ba08176021e5 Author: Barry Kane <barra@ocathain.ie> Date: Thu Aug 17 00:14:40 2023 +0100 Began implementation planning document. commit 32503cdbca74de9f454cbc5cf32a7f95d2aa3f5e Author: Barry Kane <barra@ocathain.ie> Date: Mon Aug 14 03:15:37 2023 +0100 Rename source/ to src/ for Autotools. commit 2b488477f5ce4215a999a0fc250cb6f198a0714b Author: Barry Kane <barra@ocathain.ie> Date: Mon Aug 14 02:46:43 2023 +0100 Added initial stubs for server. - Server now launches a thread to initialize Scheme, and drops into a REPL.
2023-11-05 15:14:44 +00:00
// =========================================
// | SilverMUD Server - main.c |
// | Copyright (C) 2023, Barra Ó Catháin |
// | See end of file for copyright notice. |
// =========================================
#include <errno.h>
#include <stdio.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <pthread.h>
#include <libguile.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <gnutls/gnutls.h>
#include "player-data.h"
#include "connections.h"
#include "../messages.h"
#include "scheme-integration.h"
static const int PORT = 5000;
static const int CONCURRENT_PLAYER_COUNT = 256;
int main (int argc, char ** argv)
{
// Print a welcome message:
printf("SilverMUD Server - Starting Now.\n"
"================================\n");
// Initialize Scheme:
scm_init_guile();
// Start the REPL server on a UNIX socket:
scm_c_eval_string("(begin (use-modules (system repl server))"
"(if (file-exists? \"silvermud-repl\") (delete-file \"silvermud-repl\"))"
"(spawn-server (make-unix-domain-server-socket #:path \"silvermud-repl\")))");
// Create a socket to listen for connections on:
int masterSocket = socket(AF_INET, SOCK_STREAM, 0);
if (masterSocket < 0)
{
fprintf(stderr, "Failed to create socket. Aborting.\n");
exit(EXIT_FAILURE);
}
// Allow reusing the address so that quick re-launching doesn't fail:
setsockopt(masterSocket, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
setsockopt(masterSocket, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int));
// Setup the server address struct to bind the master socket to:
struct sockaddr_in serverAddress;
memset(&serverAddress, 0, sizeof(struct sockaddr_in));
// Assign the IP address and port to the server address struct:
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddress.sin_port = htons(PORT);
// Bind the master socket to the server address:
if ((bind(masterSocket, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in))) != 0)
{
fprintf(stderr, "Failed to bind socket. Aborting.\n");
exit(EXIT_FAILURE);
}
// Begin listening:
if ((listen(masterSocket, CONCURRENT_PLAYER_COUNT)) != 0)
{
fprintf(stderr, "Failed to begin listening on the master socket. Aborting.\n");
exit(EXIT_FAILURE);
}
// Create an epoll instance for managing connections, and add the master socket to it:
int connectedClients = epoll_create(CONCURRENT_PLAYER_COUNT);
if (connectedClients < 0)
{
fprintf(stderr, "Failed to create epoll instance. Aborting.\n");
exit(EXIT_FAILURE);
}
// Setup the epoll events we want to watch for:
struct epoll_event watchedEvents;
watchedEvents.events = EPOLLIN;
watchedEvents.data.fd = masterSocket;
epoll_ctl(connectedClients, EPOLL_CTL_ADD, masterSocket, &watchedEvents);
int eventsCount = 0;
struct epoll_event events[1024];
// Setup the needed anonymous certificate for TLS:
gnutls_global_init();
gnutls_anon_server_credentials_t serverKey;
gnutls_anon_allocate_server_credentials(&serverKey);
gnutls_anon_set_server_known_dh_params(serverKey, GNUTLS_SEC_PARAM_MEDIUM);
// Create a client connection list to allow us to associate TLS sessions and sockets and players:
struct ClientConnectionList clientConnections;
// Create some structures needed to store global state:
struct PlayerList * globalPlayerList = createPlayerList();
// Start a REPL thread:
//pthread_t schemeREPLThread;
//pthread_create(&schemeREPLThread, NULL, schemeREPLHandler, NULL);
while (true)
{
do
{
eventsCount = epoll_wait(connectedClients, events, 1024, -1);
} while (eventsCount < 0 && errno == EINTR);
if (eventsCount == -1)
{
fprintf(stderr, "epoll_wait() failed. Aborting.\n");
exit(EXIT_FAILURE);
}
for (int index = 0; index < eventsCount; index++)
{
// If it's the master socket, it's a new client connecting:
if (events[index].data.fd == masterSocket)
{
// Setup a TLS Session:
gnutls_session_t * tlsSession = calloc(1, sizeof(gnutls_session_t));
gnutls_init(tlsSession, GNUTLS_SERVER);
gnutls_priority_set_direct(*tlsSession, "NORMAL:+ANON-ECDH:+ANON-DH", NULL);
gnutls_credentials_set(*tlsSession, GNUTLS_CRD_ANON, serverKey);
gnutls_handshake_set_timeout(*tlsSession, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
// Accept the connection:
int newSocket = accept(masterSocket, NULL, NULL);
gnutls_transport_set_int(*tlsSession, newSocket);
// Perform a TLS handshake:
int handshakeReturnValue = 0;
do
{
handshakeReturnValue = gnutls_handshake(*tlsSession);
} while (handshakeReturnValue < 0 && gnutls_error_is_fatal(handshakeReturnValue) == 0);
// If the handshake was unsuccessful, close the connection:
if (handshakeReturnValue < 0)
{
printf("%d", handshakeReturnValue);
fflush(stdout);
gnutls_bye(*tlsSession, 2);
shutdown(newSocket, 2);
close(newSocket);
break;
}
// Setup the epoll events we want to watch for:
watchedEvents.events = EPOLLIN;
watchedEvents.data.fd = newSocket;
epoll_ctl(connectedClients, EPOLL_CTL_ADD, newSocket, &watchedEvents);
// Add the connection to the list:
struct ClientConnection * newConnection = addNewConnection(&clientConnections, newSocket, tlsSession);
newConnection->player = createNewPlayer(newConnection);
sprintf(newConnection->player->name, "Player %02d", globalPlayerList->count + 1);
addToPlayerList(newConnection->player, globalPlayerList);
// Prepare a welcome message:
struct ServerToClientMessage welcomeMessage;
welcomeMessage.type = SYSTEM;
sprintf(welcomeMessage.content,
(clientConnections.clientCount > 1) ?
"Welcome to the server. There are %d players connected." :
"Welcome to the server. There is %d player connected.",
clientConnections.clientCount);
// Send the welcome message:
gnutls_record_send(*tlsSession, &welcomeMessage, sizeof(struct ServerToClientMessage));
// Report the new connection on the server:
printf("New connection established. %d client(s), session ID %u.\n",
clientConnections.clientCount, tlsSession);
}
else
{
// Find the corresponding TLS session:
struct ClientConnection * connection = findConnectionByFileDescriptor(&clientConnections, events[index].data.fd);
if (connection != NULL)
{
// Read the data from the TLS session:
struct ClientToServerMessage message;
int returnValue = gnutls_record_recv(*connection->tlsSession, &message, sizeof(struct ClientToServerMessage));
if (returnValue == 0 || returnValue == -10)
{
epoll_ctl(connectedClients, EPOLL_CTL_DEL, connection->fileDescriptor, &watchedEvents);
shutdown(connection->fileDescriptor, 2);
close(connection->fileDescriptor);
removeFromPlayerList(connection->player, globalPlayerList);
deallocatePlayer(&connection->player);
removeConnectionByFileDescriptor(&clientConnections, connection->fileDescriptor);
continue;
}
else if (returnValue == sizeof(struct ClientToServerMessage))
{
// TODO: BEGIN ACTUAL COMMAND INTERPRETER
// ONLY FOR DEMO
if (strncmp(message.content, "NAME ", 5) == 0 && message.content[5] != '\0')
{
strncpy(connection->player->name, &message.content[5], 64);
continue;
}
// ONLY FOR DEMO
struct ServerToClientMessage outputMessage;
// Copy the message to the output format:
outputMessage.type = LOCAL_CHAT;
strncpy(outputMessage.name, connection->player->name, 64);
strncpy(outputMessage.content, message.content, MESSAGE_CONTENT_LENGTH);
// Echo the message into all other clients: (Temporary)
struct ClientConnectionNode * currentClient = clientConnections.head;
while (currentClient != NULL)
{
gnutls_record_send(*currentClient->connection->tlsSession, &outputMessage,
sizeof(struct ServerToClientMessage));
currentClient = currentClient->next;
}
}
}
else
{
printf("Didn't find associated TLS Session!\n");
fflush(stdout);
// Remove the file descriptor from our watched set and close it:
epoll_ctl(connectedClients, EPOLL_CTL_DEL, events[index].data.fd, &watchedEvents);
close(events[index].data.fd);
removeConnectionByFileDescriptor(&clientConnections, events[index].data.fd);
}
}
}
}
// Wait for all other threads to terminate:
//pthread_join(schemeREPLThread, NULL);
// Deallocate the global state structures:
deallocatePlayerList(&globalPlayerList);
// Return a successful status code to the operating system:
return 0;
}
// ============================================
// | End of main.c, copyright notice follows. |
// ============================================
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.