From 5d9ace33872bb6283a47f11cf190cbdbabb1b1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalrish=20B=C3=A4akjen?= Date: Sat, 13 Jan 2018 14:23:17 +0100 Subject: [PATCH] Initial commit --- README.md | 66 +++ TODO.md | 6 + Tupfile | 12 + Tupfile.ini | 0 Tuprules.tup | 17 + command.c | 85 ++++ common.c | 44 ++ common.h | 14 + error.c | 46 ++ error.h | 15 + logmsg.c | 30 ++ logmsg.h | 14 + server.c | 992 ++++++++++++++++++++++++++++++++++++++++ server.h | 21 + service.c | 193 ++++++++ service.h | 8 + toolchains.tup/gnu.tup | 20 + toolchains.tup/msvc.tup | 38 ++ 18 files changed, 1621 insertions(+) create mode 100644 README.md create mode 100644 TODO.md create mode 100644 Tupfile create mode 100644 Tupfile.ini create mode 100644 Tuprules.tup create mode 100644 command.c create mode 100644 common.c create mode 100644 common.h create mode 100644 error.c create mode 100644 error.h create mode 100644 logmsg.c create mode 100644 logmsg.h create mode 100644 server.c create mode 100644 server.h create mode 100644 service.c create mode 100644 service.h create mode 100644 toolchains.tup/gnu.tup create mode 100644 toolchains.tup/msvc.tup diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcf6f8a --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Lappenchat server for Windows + +Lappenchat server targeting Windows, implemented as a command and a service and using only native Windows APIs. + + +## Was zum Teufel? +Für das Semesterprojekt des vom Professor Siegfried Rump erteilten Lehrfaches Prozedurale Programmierung, das im ersten Semester stattfindet, haben wir uns für eine Chat-Implementierung entschieden. Das Projekt besteht aus einem Server, der Nachrichten von den Clients bekommt und weitergibt, und einigen Clients, die Kontakt mit dem Server aufnehmen und die dem Benutzer dazu dienen, mit anderen zu kommunizieren. Dies ist nun der Windows Server. Der Name begreift das etwas beleidigende Wort »Lappen« ein, das einem der Mitglieder des Projektteams an einem unseligen Tage von sehr witzigen Kommilitonen beschert wurde. + +## Installation + +### Dependencies +The programs don't require any libraries beyond those included in a standard Windows installation. They both have to be linked against `ws2_32.dll`. Additionally, the service requires `advapi32.dll`. + +### Configuration +The build scripts accompanying this program and used in its development include defaults which should work in all cases. Thus, for a quick check of this project, you could possibly do without setting any build parameter. + +Should you want to customize the build parameters or to adjust them to suit your environment, you can do so in a `tup.config` file. + +### Build +The [tup build system](http://gittup.org/tup/) manages the build process. + +### Installation +The server is implemented both as a command and as a Windows service. + +The command is mainly meant for a quick check. Should it be desired to install it, however, its installation involves nothing more than copying it to the desired location. + +As for the service, its installation comprises the usual setup of a new service, which can be carried out with the SC.EXE command as follows: + + $ sc config lappenchat-server DisplayName= "Lappenchat server" depend= tcpip binPath= "exePath" + +The placeholder _exePath_ is the absolute path of the Lappenchat server executable. + + +## Usage +The command can be managed from a command prompt. To start it: + + $ lappenchat-server-command [OPTIONS] + +To stop it, hit CTRL-C. + +The service can be managed as any other Windows service. + +In a command prompt: + + $ sc start lappenchat-server [OPTIONS] + +Then, to stop it: + + $ sc stop lappenchat-server + +In both cases, _OPTIONS_ is a placeholder for any options you might want to pass to the server. + +### Options +Options are specified as service start parameters, in the following format: the option code is preceded by a single dash and any required arguments are provided as separate parameters following it. An example: + + $ sc start lappenchat-server -l logFilePath -p Port -t nThreads + +A list of the options currently supported follows: + +| Code | Availability | Meaning +|-------|-----------------|---------------------- +| l | service | **Path to the log file**. If this option is not specified, logs will not be saved anywhere. +| p | command+service | **Port number to listen on**. The default is 3144. +| t | command+service | **Number of threads** to spawn and use in handling connection requests and server traffic. + +Some options are only supported by the service. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..fbccbe9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +- [ ] Mark suitable pointers with the `restrict` keyword +- [ ] Extract the client disconnect logic into a separate function +- [ ] Implement a proper memory strategy that doesn't impose a limit on the number of connected users and, if possible, doesn't require that much mutually-exclusive access from different threads (i.e. one that's more multithread-friendly) +- [ ] Use a memory pool for the message buffers +- [ ] Implement another strategy using AcceptEx instead of accept +- [ ] Send messages asynchronously (i.e. use WSASend instead of send) diff --git a/Tupfile b/Tupfile new file mode 100644 index 0000000..3f37700 --- /dev/null +++ b/Tupfile @@ -0,0 +1,12 @@ +include_rules + + +: foreach common.c server.c error.c logmsg.c |> !cc |> {objs} + +: command.c |> !cc |> {command_obj} +LIBS=$(LIBS_COMMAND) +: {command_obj} {objs} |> !ld |> lappenchat-server-command.exe + +: service.c |> !cc |> {service_obj} +LIBS=$(LIBS_SERVICE) +: {service_obj} {objs} |> !ld |> lappenchat-server-service.exe diff --git a/Tupfile.ini b/Tupfile.ini new file mode 100644 index 0000000..e69de29 diff --git a/Tuprules.tup b/Tuprules.tup new file mode 100644 index 0000000..23fc8dc --- /dev/null +++ b/Tuprules.tup @@ -0,0 +1,17 @@ +.gitignore + + +ifdef TOOLCHAIN + TOOLCHAIN=@(TOOLCHAIN) +else + ifeq (@(TUP_PLATFORM),win32) + # Use Microsoft's native compiler if building on Windows + TOOLCHAIN=msvc + else + # Otherwise, go for MinGW + TOOLCHAIN=gnu + endif +endif + +# Include the appropriate toolchain definition file +include toolchains.tup/$(TOOLCHAIN).tup diff --git a/command.c b/command.c new file mode 100644 index 0000000..9c135de --- /dev/null +++ b/command.c @@ -0,0 +1,85 @@ +#include +#include +#include "logmsg.h" +#include "error.h" +#include "common.h" +#include "server.h" + + +static HANDLE stop_event; + +BOOL WINAPI ConsoleCtrlHandler +( + DWORD signal +) +{ + switch ( signal ) + { + case CTRL_C_EVENT: + return SetEvent(stop_event); + default: + return 0; + } +} + +int main +( + int argc, + char * * argv +) +{ + int rv = 0; + char parameter = 0; + struct lappenchat_server_options lcso = { 0 }; + + logout = stderr; + + for ( char * * arg_cur = argv, * * const argv_end = argv+argc ; arg_cur != argv_end ; ++arg_cur ) + { + char * const arg = *arg_cur; + if ( parameter ) + { + switch ( parameter ) + { + /* FIXME: strtol parses a sign ('+' or '-') as well. + * This should actually be prohibited here, as a "negative + * port" or a "negative number of threads" don't make + * sense. */ + case 'p': + /* Here, we should also check that the number + * provided doesn't exceed the maximum port #. */ + lcso.port = (u_short)strtol(arg, NULL, 10); + break; + case 't': + lcso.threads = strtol(arg, NULL, 10); + break; + } + parameter = 0; + } + else + if ( *arg == '-' ) + parameter = arg[1]; + } + + if ( stop_event = CreateEvent(NULL, TRUE, FALSE, NULL) ) + { + if ( SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE) ) + { + if ( !lcso.port ) + lcso.port = 3144; + + if ( !lcso.threads ) + lcso.threads = get_proc_n(); + + rv = start_server(lcso, stop_event); + } + else + winapi_perror("couldn't set console control handler"); + + CloseHandle(stop_event); + } + else + winapi_perror("couldn't create stop event object"); + + return !rv; +} diff --git a/common.c b/common.c new file mode 100644 index 0000000..5fcb2a2 --- /dev/null +++ b/common.c @@ -0,0 +1,44 @@ +#include "common.h" + +#include +#include +#include "logmsg.h" +#include "error.h" +#include "server.h" + + +int start_server +( + struct lappenchat_server_options lcso, + HANDLE stop_event +) +{ + int rv; + if ( WSAStartup(MAKEWORD(2,2), &lcso.wsa_data) == 0 ) + { + logmsgf("successfully initialized Winsock\nWinsock version offered: %u.%u\nWinsock implementation description: %s\n", LOBYTE(lcso.wsa_data.wVersion), HIBYTE(lcso.wsa_data.wVersion), lcso.wsa_data.szDescription); + + rv = lappenchat_server(lcso, stop_event); + + if ( WSACleanup() == 0 ) + logmsg("successfully terminated use of Winsock"); + else + wsa_perror("couldn't terminate use of Winsock"); + } + else + { + logmsg("couldn't initialize Winsock"); + rv = 0; + } + + return rv; +} + +size_t +get_proc_n +( void ) +{ + SYSTEM_INFO system_info; + GetSystemInfo(&system_info); + return system_info.dwNumberOfProcessors; +} diff --git a/common.h b/common.h new file mode 100644 index 0000000..8ce62af --- /dev/null +++ b/common.h @@ -0,0 +1,14 @@ +#include // size_t +#include +#include // HANDLE +#include "server.h" // struct lappenchat_server_options + + +int start_server +( + struct lappenchat_server_options, + HANDLE +); + +size_t get_proc_n +(void); diff --git a/error.c b/error.c new file mode 100644 index 0000000..d981340 --- /dev/null +++ b/error.c @@ -0,0 +1,46 @@ +#include "error.h" + +#include +#include "logmsg.h" + + +void win_perror +( + const char * const msg, + int error_code +) +{ + LPTSTR formatted_error_code; + + if ( FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error_code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), + (LPTSTR)&formatted_error_code, + 0, + NULL + ) == 0 ) + logmsgf("%s (%i)\n", msg, error_code); + else + { + logmsgf("%s: %i: %s", msg, error_code, formatted_error_code); + LocalFree(formatted_error_code); + } +} + +void wsa_perror +( + const char * const msg +) +{ + win_perror(msg, WSAGetLastError()); +} + +void winapi_perror +( + const char * const msg +) +{ + win_perror(msg, GetLastError()); +} diff --git a/error.h b/error.h new file mode 100644 index 0000000..0793e63 --- /dev/null +++ b/error.h @@ -0,0 +1,15 @@ +void win_perror +( + const char * const msg, + int error_code +); + +void wsa_perror +( + const char * const msg +); + +void winapi_perror +( + const char * const msg +); diff --git a/logmsg.c b/logmsg.c new file mode 100644 index 0000000..73811ac --- /dev/null +++ b/logmsg.c @@ -0,0 +1,30 @@ +#include "logmsg.h" + +#include +#include + + +FILE * logout; + +void logmsg +( + const char * const msg +) +{ + fputs(msg, logout); + fputc('\n', logout); + fflush(logout); +} + +void logmsgf +( + const char * const fmt, + ... +) +{ + va_list arguments; + va_start(arguments, fmt); + vfprintf(logout, fmt, arguments); + fflush(logout); + va_end(arguments); +} diff --git a/logmsg.h b/logmsg.h new file mode 100644 index 0000000..8c8e153 --- /dev/null +++ b/logmsg.h @@ -0,0 +1,14 @@ +#include + + +extern FILE * logout; + +void logmsg +( + const char * const msg +); +void logmsgf +( + const char * const fmt, + ... +); diff --git a/server.c b/server.c new file mode 100644 index 0000000..d262d13 --- /dev/null +++ b/server.c @@ -0,0 +1,992 @@ +#include "server.h" + +#include +#include +#include +#include +#include +#include "logmsg.h" +#include "error.h" + +#define SERVER_SOCKETS 2 +#define max_clients 62 + + +static void +close_socket +( + SOCKET s, + const char * const error_msg +) +{ + if ( closesocket(s) == SOCKET_ERROR ) + wsa_perror(error_msg); +} + +enum Phase { + phase_getting_nickname_length, + phase_getting_nickname, + phase_getting_message_length, + phase_getting_message +}; + +/* This structure contains information about + * a single client. It is thus to be associated + * to the client handle when it (the client handle) + * gets attached to the completion port (with + * CreateIoCompletionPort). */ +typedef struct { + char used; + SOCKET socket; + enum Phase phase; + char nickname[32]; + unsigned char nickname_length; +} ClientData; + +static size_t +broadcast_message +( + ClientData * cur, + const char * buffer, + const int size +) +{ + size_t clients_sent = 0; + for ( ClientData * const end = cur + max_clients ; cur != end ; ++cur ) + { + if ( cur->used ) + { + int rv; + size_t sent = 0; +send_more: + if ( (rv = send(cur->socket, buffer, size, 0)) != SOCKET_ERROR ) + { + sent += rv; + if ( sent == size ) + ++clients_sent; + else + { + assert(sent < size); + goto send_more; + } + } + else + wsa_perror("couldn't send message to client"); + } + } + return clients_sent; +} + +static SOCKET +get_ipv4_socket +( + u_short port +) +{ + SOCKET ss_ipv4 = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); + if ( ss_ipv4 != INVALID_SOCKET ) + { + logmsg("successfully created IPv4 socket"); + + struct sockaddr_in localhost_ipv4 = { + .sin_family = AF_INET, + .sin_addr.s_addr = INADDR_ANY, + .sin_port = htons(port) + }; + if ( bind(ss_ipv4, (struct sockaddr *)&localhost_ipv4, sizeof(localhost_ipv4)) != SOCKET_ERROR ) + logmsg("successfully bound IPv4 socket to the local addresses"); + else + { + wsa_perror("couldn't bind IPv4 socket"); + closesocket(ss_ipv4); + ss_ipv4 = INVALID_SOCKET; + } + } + else + wsa_perror("couldn't create IPv4 socket"); + + return ss_ipv4; +} + +static SOCKET +get_ipv6_socket +( + u_short port +) +{ + SOCKET ss_ipv6 = WSASocketW(AF_INET6, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); + if ( ss_ipv6 != INVALID_SOCKET ) + { + logmsg("successfully created IPv6 socket"); + + /* By disabling the IPV6_V6ONLY option, it would be possible + * to handle both IPv4 and IPv6 with the same socket. */ + //setsockopt(ss_ipv6, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)(DWORD[]){0}, sizeof(DWORD)); + + struct sockaddr_in6 localhost_ipv6 = { + .sin6_family = AF_INET6, + .sin6_addr = in6addr_any, + .sin6_port = htons(port), + .sin6_scope_id = 0 + }; + if ( bind(ss_ipv6, (struct sockaddr *)&localhost_ipv6, sizeof(localhost_ipv6)) != SOCKET_ERROR ) + logmsg("successfully bound IPv6 socket to the local addresses"); + else + { + wsa_perror("couldn't bind IPv6 socket"); + closesocket(ss_ipv6); + ss_ipv6 = INVALID_SOCKET; + } + } + else + wsa_perror("couldn't create IPv6 socket"); + + return ss_ipv6; +} + +static int +setup_listen_events +( + SOCKET * server_sockets, + size_t server_sockets_n, + WSAEVENT * event_handles_1, + WSAEVENT * event_handles_2 +) +{ + SOCKET * const list_end = server_sockets + server_sockets_n; + for ( ; server_sockets != list_end ; ++server_sockets ) + { + WSAEVENT event_handle = WSACreateEvent(); + if ( event_handle != WSA_INVALID_EVENT ) + { + if ( WSAEventSelect(*server_sockets, event_handle, FD_ACCEPT) == 0 ) + { + *event_handles_1++ = event_handle; + *event_handles_2++ = event_handle; + } + else + return 0; + } + else + return 0; + } + + return 1; +} + +static int +lappenchat_server_inner_event +( + HANDLE stop_event, + SOCKET * server_sockets, + DWORD server_sockets_n, + struct sockaddr * * client_addr, + int * client_addr_sizes +) +{ + int rv; + + static_assert(SERVER_SOCKETS + max_clients <= WSA_MAXIMUM_WAIT_EVENTS, "the sum of the maximum number of sockets used by the server and the maximum number of client sockets exceeds the maximum supported by Winsock"); + WSAEVENT event_handles_1[max_clients + SERVER_SOCKETS]; + WSAEVENT event_handles_2[max_clients + SERVER_SOCKETS]; + WSAEVENT * event_handles_fore; + WSAEVENT * event_handles_back; + + rv = 0; + event_handles_fore = event_handles_1; + event_handles_back = event_handles_2; + + if ( setup_listen_events(server_sockets, server_sockets_n, event_handles_1, event_handles_2) ) + { + SOCKET client_sockets_1[max_clients]; + SOCKET client_sockets_2[max_clients]; + SOCKET * client_sockets_fore; + SOCKET * client_sockets_back; + DWORD clients_n; + + client_sockets_fore = client_sockets_1; + client_sockets_back = client_sockets_2; + clients_n = 0; + + while ( WaitForSingleObject(stop_event, 0) != WAIT_OBJECT_0 ) + { + DWORD poll_code; + WSAEVENT * client_event_handles_fore; + WSAEVENT * client_event_handles_back; + + client_event_handles_fore = event_handles_fore + server_sockets_n; + client_event_handles_back = event_handles_back + server_sockets_n; + + poll_code = WSAWaitForMultipleEvents(server_sockets_n + clients_n, event_handles_fore, FALSE, 3000, FALSE); + if ( poll_code >= WSA_WAIT_EVENT_0 && poll_code <= WSA_WAIT_EVENT_0+server_sockets_n+clients_n+1 ) + { + WSAEVENT * client_event_handles_end; + const size_t event_index = poll_code - WSA_WAIT_EVENT_0; + + client_event_handles_end = client_event_handles_fore + clients_n; + + if ( event_index < server_sockets_n ) + { + /* A new client wants to connect through one of the + * server sockets. Let's check them and handle the + * request. */ + + SOCKET * server_socket; + WSAEVENT * event_handle; + for ( event_handle = event_handles_fore, server_socket = server_sockets ; event_handle != client_event_handles_fore ; ++server_socket, ++event_handle ) + { + WSANETWORKEVENTS wsa_events; + + WSAEnumNetworkEvents(*server_socket, *event_handle, &wsa_events); + WSAResetEvent(*event_handle); + + if ( wsa_events.lNetworkEvents & FD_ACCEPT ) + { + logmsg("connection request received"); + + SOCKET client_socket = accept(*server_socket, client_addr[event_index], &client_addr_sizes[event_index]); + if ( client_socket != INVALID_SOCKET ) + { + WSAEVENT client_event = WSACreateEvent(); + if ( client_event != WSA_INVALID_EVENT ) + { + if ( WSAEventSelect(client_socket, client_event, FD_READ | FD_CLOSE) == 0 ) + { + client_sockets_fore[clients_n] = client_socket; + client_event_handles_fore[clients_n] = client_event; + + ++client_event_handles_end; + ++clients_n; + + logmsg("client connected"); + } + else + { + wsa_perror("WSAEventSelect failed"); + + WSACloseEvent(client_event); + closesocket(client_socket); + } + } + else + { + wsa_perror("WSACreateSelect failed"); + + closesocket(client_socket); + } + } + else + { + wsa_perror("couldn't accept connection request"); + } + } + } + } + + { + WSAEVENT * out_event; + SOCKET * out_socket; + WSAEVENT * cur; + SOCKET * cur_socket; + + out_event = client_event_handles_back; + out_socket = client_sockets_back; + + for ( cur = client_event_handles_fore, cur_socket = client_sockets_fore ; cur != client_event_handles_end ; ++cur, ++cur_socket ) + { + WSANETWORKEVENTS wsa_events; + WSAEVENT client_event_handle; + SOCKET client_socket; + + client_event_handle = *cur; + client_socket = *cur_socket; + + WSAEnumNetworkEvents(client_socket, client_event_handle, &wsa_events); + WSAResetEvent(client_event_handle); + + if ( wsa_events.lNetworkEvents & FD_READ ) + { + char buffer[512]; + int size = 512; + int recv_size = recv(client_socket, buffer, size, 0); + if ( recv_size != SOCKET_ERROR ) + { + /* + size_t message_length = *buffer; + message_length <<= 8; + message_length += buffer[1]; + */ + + buffer[recv_size] = '\0'; + logmsg(buffer); + //broadcast_message(client_sockets_fore, clients_n, buffer, size); + } + else + { + wsa_perror("couldn't receive packet from client"); + } + /* + char c; + int recv_size = recv(client_socket, &c, 1, 0); + if ( recv_size != SOCKET_ERROR ) + { + char msg[] = "Received: "; + msg[10] = c; + logmsg(msg); + } + else + { + wsa_perror("couldn't receive packet from client"); + } + */ + } + + if ( wsa_events.lNetworkEvents & FD_CLOSE ) + { + close_socket(client_socket, "couldn't close client socket"); + WSACloseEvent(client_event_handle); + + --clients_n; + + logmsg("client disconnected"); + } + else + { + *out_event++ = client_event_handle; + *out_socket++ = client_socket; + } + } + } + + /* Swap fore- and back-pointers */ + { + WSAEVENT * temp_e; + SOCKET * temp_s; + + temp_e = event_handles_fore; + temp_s = client_sockets_fore; + + event_handles_fore = event_handles_back; + client_sockets_fore = client_sockets_back; + + event_handles_back = temp_e; + client_sockets_back = temp_s; + } + } + } + + logmsg("exited server loop"); + + { + SOCKET * cur; + SOCKET * const end = client_sockets_fore + clients_n; + for ( cur = client_sockets_fore ; cur != end ; ++cur ) + closesocket(*cur); + } + + logmsg("closed remaining sockets"); + logmsg("ended remaining connections"); + + rv = 1; + } + + { + WSAEVENT * cur; + WSAEVENT * const end = event_handles_fore + server_sockets_n + max_clients; + for ( cur = event_handles_fore ; cur != end ; ++cur ) + { + if ( *cur != WSA_INVALID_EVENT ) + WSACloseEvent(*cur); + } + } + + logmsg("freed remaining event objects"); + + return rv; +} + +/* An object of this structure is passed to + * send and receive routines and got back with + * GetQueuedCompletionPortStatus. It contains + * information pertaining to an I/O operation. */ +typedef struct { + /* This member must be the first one - either + * that, or use CONTAINING_RECORD */ + WSAOVERLAPPED wsa_overlapped; + char message_length; + unsigned char received; + char buffer[290]; +} OperationData; + +/* An object of this type shall be shared + * by the main thread and the worker threads. */ +typedef struct { + HANDLE completion_port; + HANDLE client_pool_mutex; + ClientData clients[max_clients]; +} SharedStructures; + +DWORD WINAPI +worker_thread +( + LPVOID data +) +{ + SharedStructures * shared = (SharedStructures *)data; + DWORD thread_id = GetCurrentThreadId(); + DWORD flags = 0; + + logmsgf("worker thread #%"PRIuLEAST32": ready\n", thread_id); + + for ( ; ; ) + { + DWORD size; + ClientData * client_data; + OperationData * operation_data; + if ( GetQueuedCompletionStatus(shared->completion_port, &size, (PULONG_PTR)&client_data, (LPOVERLAPPED *)&operation_data, INFINITE) ) + { + logmsgf("worker thread #%"PRIuLEAST32": completion notification dequeued successfully\n", thread_id); + if ( size ) + { + switch ( client_data->phase ) + { + case phase_getting_nickname_length: + { + assert(size == 1); + + logmsgf("new client's nickname length: %zu\n", client_data->nickname_length); + + client_data->phase = phase_getting_nickname; + + operation_data->received = 0; + + /* Queue another recv to get the part of the nickname + * that we are still missing */ + WSABUF buffer_info = { + .buf = client_data->nickname, + .len = client_data->nickname_length + }; + if ( WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL) == SOCKET_ERROR ) + { + int error_code = WSAGetLastError(); + if ( error_code != WSA_IO_PENDING ) + { + win_perror("couldn't queue next recv", error_code); + /* FIXME: disconnect client. We couldn't queue another + * recv operation, so it's not like we'll be getting + * any further message from this client. */ + } + } + + break; + } + + case phase_getting_nickname: + { + operation_data->received += (char)size; + + logmsgf("received %zu additional bytes of nickname, until now: %.*s\n", operation_data->received, operation_data->received, client_data->nickname); + + if ( operation_data->received == client_data->nickname_length ) + { + logmsgf("new client connected: %.*s\n", client_data->nickname_length, client_data->nickname); + + client_data->phase = phase_getting_message_length; + + /* Queue a recv for the client's first message */ + WSABUF buffer_info = { + .buf = &(operation_data->message_length), + .len = 1 + }; + if ( WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL) == SOCKET_ERROR ) + { + int error_code = WSAGetLastError(); + if ( error_code != WSA_IO_PENDING ) + { + win_perror("couldn't queue next recv", error_code); + /* FIXME: disconnect client. We couldn't queue another + * recv operation, so it's not like we'll be getting + * any further message from this client. */ + } + } + } + else + { + assert(operation_data->received < client_data->nickname_length); + + /* Queue another recv to get the part of the nickname + * that we are still missing */ + WSABUF buffer_info = { + .buf = client_data->nickname + operation_data->received, + .len = client_data->nickname_length - operation_data->received + }; + int error_code = WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL); + assert(error_code == SOCKET_ERROR); + error_code = WSAGetLastError(); + if ( error_code != WSA_IO_PENDING ) + { + win_perror("couldn't queue next recv", error_code); + /* FIXME: disconnect client. We couldn't queue another + * recv operation, so it's not like we'll be getting + * any further message from this client. */ + } + } + + break; + } + + case phase_getting_message_length: + { + logmsgf("worker thread #%"PRIuLEAST32": %.*s reports having sent a message %i bytes long\n", thread_id, client_data->nickname_length, client_data->nickname, operation_data->message_length); + + /* Reset received for the next (first) message */ + operation_data->received = 0; + + client_data->phase = phase_getting_message; + + WSABUF buffer_info = { + .buf = operation_data->buffer + sizeof(client_data->nickname) + 2, + .len = operation_data->message_length + }; + if ( WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL) == SOCKET_ERROR ) + { + int error_code = WSAGetLastError(); + if ( error_code != WSA_IO_PENDING ) + { + win_perror("couldn't queue next recv", error_code); + /* FIXME: disconnect client. We couldn't queue another + * recv operation, so it's not like we'll be getting + * any further message from this client. */ + } + } + + break; + } + + case phase_getting_message: + { + operation_data->received += size; + + if ( operation_data->received == operation_data->message_length ) + { + logmsgf("worker thread #%"PRIuLEAST32": got complete message from %.*s (%u bytes long): %.*s\n", thread_id, client_data->nickname_length, client_data->nickname, operation_data->message_length, operation_data->message_length, operation_data->buffer+1+sizeof(client_data->nickname)+1); + + *(operation_data->buffer + sizeof(client_data->nickname) - client_data->nickname_length) = client_data->nickname_length; + memcpy(operation_data->buffer + sizeof(client_data->nickname) - client_data->nickname_length + 1, client_data->nickname, client_data->nickname_length); + *(operation_data->buffer + sizeof(client_data->nickname) + 1) = operation_data->message_length; + + if ( WaitForSingleObject(shared->client_pool_mutex, INFINITE) == WAIT_OBJECT_0 ) + { + size_t clients_sent = broadcast_message(shared->clients, operation_data->buffer + sizeof(client_data->nickname) - client_data->nickname_length, 1 + client_data->nickname_length + 1 + operation_data->message_length); + + ReleaseMutex(shared->client_pool_mutex); + + if ( clients_sent ) + logmsgf("worker thread #%"PRIuLEAST32": message sent to %zu clients\n", thread_id, clients_sent); + else + logmsgf("worker thread #%"PRIuLEAST32": couldn't send message to any client\n", thread_id); + } + else + winapi_perror("couldn't lock client pool mutex"); + + client_data->phase = phase_getting_message_length; + + /* Queue a recv for the client's next message */ + WSABUF buffer_info = { + .buf = &(operation_data->message_length), + .len = 1 + }; + if ( WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL) == SOCKET_ERROR ) + { + int error_code = WSAGetLastError(); + if ( error_code != WSA_IO_PENDING ) + { + win_perror("couldn't queue next recv", error_code); + /* FIXME: disconnect client. We couldn't queue another + * recv operation, so it's not like we'll be getting + * any further message from this client. */ + } + } + } + else + { + assert(operation_data->received < operation_data->message_length); + + logmsgf("worker thread #%"PRIuLEAST32": getting message from %.*s: got %i bytes until now\n", thread_id, client_data->nickname_length, client_data->nickname, operation_data->received); + + WSABUF buffer_info = { + .buf = operation_data->buffer + sizeof(client_data->nickname) + 2 + operation_data->received, + .len = operation_data->message_length - operation_data->received + }; + if ( WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL) == SOCKET_ERROR ) + { + int error_code = WSAGetLastError(); + if ( error_code != WSA_IO_PENDING ) + { + win_perror("couldn't queue next recv", error_code); + /* FIXME: disconnect client. We couldn't queue another + * recv operation, so it's not like we'll be getting + * any further message from this client. */ + } + } + } + + break; + } + } + } + else + { + logmsg("client disconnected"); + if ( WaitForSingleObject(shared->client_pool_mutex, INFINITE) == WAIT_OBJECT_0 ) + { + client_data->used = 0; + closesocket(client_data->socket); + + ReleaseMutex(shared->client_pool_mutex); + + free(operation_data); + + logmsg("client object released"); + } + } + } + else + { + DWORD error_code = GetLastError(); + switch ( error_code ) + { + case ERROR_ABANDONED_WAIT_0: + logmsgf("worker thread #%"PRIuLEAST32": received request to shut down\n", thread_id); + return EXIT_SUCCESS; + case ERROR_NETNAME_DELETED: + logmsg("client disconnected"); + if ( WaitForSingleObject(shared->client_pool_mutex, INFINITE) == WAIT_OBJECT_0 ) + { + client_data->used = 0; + client_data->nickname_length = 0; + closesocket(client_data->socket); + + ReleaseMutex(shared->client_pool_mutex); + + free(operation_data); + + logmsg("client object released"); + } + break; + default: + win_perror("couldn't retrieve completion packet from the completion port queue", error_code); + } + } + } +} + +static ClientData * +find_free_slot +( + ClientData * cur +) +{ + for ( ClientData * const end = cur + max_clients ; cur != end ; ++cur ) + if ( !cur->used ) + return cur; + + return NULL; +} + +static int +lappenchat_server_inner_completionport +( + HANDLE stop_event, + SOCKET * server_sockets, + DWORD server_sockets_n, + size_t threads +) +{ + int rv = 1; + WSAEVENT event_handles[SERVER_SOCKETS + 1]; + SharedStructures shared = {0}; + SOCKET * const sockets_end = server_sockets + server_sockets_n; + WSAEVENT * const server_event_handles = event_handles + 1; + WSAEVENT * const event_handles_end = event_handles + server_sockets_n + 1; + + { + WSAEVENT * event_handles_ptr = server_event_handles; + for ( SOCKET * sockets_cur = server_sockets ; sockets_cur != sockets_end ; ++sockets_cur ) + { + WSAEVENT event_handle = WSACreateEvent(); + if ( event_handle != WSA_INVALID_EVENT ) + { + if ( WSAEventSelect(*sockets_cur, event_handle, FD_ACCEPT) == 0 ) + *event_handles_ptr++ = event_handle; + else + rv = 0; + } + else + { + winapi_perror("couldn't create event for one of the server sockets"); + rv = 0; + } + } + } + + if ( shared.completion_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0) ) + { + size_t created = 0; + /* We create threads-1 threads because our main thread does do something + * and shall thus count as well. What it does is handle incoming connection + * requests and accept them. This we could handle in the worker threads too + * if we used AcceptEx. */ + for ( size_t i = 0, threads_to_create = threads-1 ; i < threads_to_create ; ++i ) + { + HANDLE thread; + if ( thread = CreateThread(NULL, 0, worker_thread, &shared, 0, NULL) ) + { + ++created; + CloseHandle(thread); + } + else + winapi_perror("couldn't create worker thread"); + } + + /* Don't fail if at least one thread could be created */ + if ( created ) + logmsgf("successfully spawned %zu threads\n", created); + else + { + logmsg("error: couldn't create any worker threads"); + rv = 0; + } + } + else + { + winapi_perror("couldn't create completion port"); + rv = 0; + } + + if ( !(shared.client_pool_mutex = CreateMutex(NULL, FALSE, NULL)) ) + { + winapi_perror("couldn't create mutex for the client pool"); + rv = 0; + } + + if ( rv ) + { + /* Set the sockets in listening state */ + { + size_t count = 0; + for ( SOCKET * cur = server_sockets ; cur != sockets_end ; ++cur ) + { + if ( listen(*cur, SOMAXCONN) != SOCKET_ERROR ) + ++count; + else + wsa_perror("couldn't put server socket to listen"); + } + if ( count == server_sockets_n ) + logmsgf("all %zu server sockets listening\n", count); + else if ( count > 0 ) + logmsgf("%zu server sockets listening\n", count); + else + { + assert(count == 0); + logmsg("error: couldn't put any of the server sockets to listen"); + rv = 0; + } + } + + if ( rv ) + { + DWORD events_n = server_sockets_n + 1; + DWORD max_to_compare = WSA_WAIT_EVENT_0 + events_n + 1; + DWORD flags = 0; // FIXME !!! + + *event_handles = stop_event; + + /* Perhaps we should report SERVICE_RUNNING now */ + + for ( ; ; ) + { + assert(event_handles[0] == stop_event); + DWORD poll_code = WSAWaitForMultipleEvents(events_n, event_handles, FALSE, WSA_INFINITE, FALSE); + if ( poll_code >= WSA_WAIT_EVENT_0 && poll_code <= max_to_compare ) + { + const size_t event_index = poll_code - WSA_WAIT_EVENT_0; + if ( event_index == 0 ) + { + logmsg("server shutdown event set"); + break; + } + else + { + SOCKET * server_socket; + WSAEVENT * event_handle; + for ( event_handle = server_event_handles, server_socket = server_sockets ; event_handle != event_handles_end ; ++server_socket, ++event_handle ) + { + WSANETWORKEVENTS wsa_events; + + WSAEnumNetworkEvents(*server_socket, *event_handle, &wsa_events); + WSAResetEvent(*event_handle); + + if ( wsa_events.lNetworkEvents & FD_ACCEPT ) + { + logmsg("received connection request"); + + if ( WaitForSingleObject(shared.client_pool_mutex, INFINITE) == WAIT_OBJECT_0 ) + { + ClientData * const client_data = find_free_slot(shared.clients); + if ( client_data ) + { + client_data->phase = phase_getting_nickname_length; + client_data->socket = accept(*server_socket, NULL, NULL); + if ( client_data->socket != INVALID_SOCKET ) + { + /* FIXME: log connector address ("accepted connection attempt from x.x.x.x / y:y:y: ...") */ + logmsg("accepted connection request"); + + if ( CreateIoCompletionPort((HANDLE)client_data->socket, shared.completion_port, (ULONG_PTR)client_data, 0) ) + { + /* calloc ensures that the memory chunk will be zero-filled */ + OperationData * const operation_data = calloc(1, sizeof(*operation_data)); + if ( operation_data ) + { + logmsg("new client attached to the completion port"); + + WSABUF buffer_info = { + .buf = &(client_data->nickname_length), + .len = 1 + }; + + operation_data->received = 0; + + switch ( WSARecv(client_data->socket, &buffer_info, 1, NULL, &flags, &(operation_data->wsa_overlapped), NULL) ) + { + case SOCKET_ERROR: + { + int error_code = WSAGetLastError(); + if ( error_code == WSA_IO_PENDING ) + { + case 0: + client_data->used = 1; + } + else + { + win_perror("couldn't queue initial recv", error_code); + closesocket(client_data->socket); + free(operation_data); + } + } + } + } + else + { + logmsg("couldn't allocate memory for initial recv's data"); + closesocket(client_data->socket); + } + } + else + { + winapi_perror("couldn't attach client connection socket to the completion port"); + closesocket(client_data->socket); + } + } + else + { + wsa_perror("couldn't accept connection request"); + } + } + else + { + logmsg("no free slot for new client's data"); + } + + ReleaseMutex(shared.client_pool_mutex); + } + } + } + } + } + } + + logmsg("main server loop exited"); + } + } + + if ( shared.client_pool_mutex ) + { + if ( !CloseHandle(shared.client_pool_mutex) ) + winapi_perror("couldn't dispose of client pool mutex"); + } + + if ( shared.completion_port ) + { + if ( CloseHandle(shared.completion_port) ) + logmsg("successfully disposed of completion port"); + else + winapi_perror("couldn't dispose of completion port"); + } + + for ( WSAEVENT * cur = server_event_handles ; cur != event_handles_end ; ++cur ) + { + if ( *cur != WSA_INVALID_EVENT ) + { + if ( WSACloseEvent(*cur) == FALSE ) + wsa_perror("couldn't close server event object"); + } + } + + return rv; +} + +int lappenchat_server +( + struct lappenchat_server_options lcso, + HANDLE stop_event +) +{ + int rv = 0; + if ( LOBYTE(lcso.wsa_data.wVersion) == 2 && HIBYTE(lcso.wsa_data.wVersion) == 2 ) + { + /* The server handles connections over a variety of protocols, + * each of which is handled with a dedicated socket. */ + SOCKET server_sockets[SERVER_SOCKETS]; + SOCKET * server_sockets_entry; + + server_sockets_entry = server_sockets; + + SOCKET ss_ipv4 = get_ipv4_socket(lcso.port); + if ( ss_ipv4 != INVALID_SOCKET ) + *server_sockets_entry++ = ss_ipv4; + + SOCKET ss_ipv6 = get_ipv6_socket(lcso.port); + if ( ss_ipv6 != INVALID_SOCKET ) + *server_sockets_entry++ = ss_ipv6; + + if ( server_sockets_entry != server_sockets ) + { + /* At least one socket has been set up successfully */ + + rv = lappenchat_server_inner_completionport(stop_event, server_sockets, (DWORD)(server_sockets_entry-server_sockets), lcso.threads); + + if ( ss_ipv4 != INVALID_SOCKET ) + { + if ( closesocket(ss_ipv4) != SOCKET_ERROR ) + logmsg("successfully closed IPv4 socket"); + else + wsa_perror("couldn't close IPv4 socket"); + } + if ( ss_ipv6 != INVALID_SOCKET ) + { + if ( closesocket(ss_ipv6) != SOCKET_ERROR ) + logmsg("successfully closed IPv6 socket"); + else + wsa_perror("couldn't close IPv6 socket"); + } + } + else + logmsg("couldn't establish any socket for the server"); + } + else + logmsg("unsuitable Winsock version"); + + return rv; +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..42ccdac --- /dev/null +++ b/server.h @@ -0,0 +1,21 @@ +#ifndef SERVER_H +#define SERVER_H + +#include // size_t +#include // WSADATA, u_short +#include // HANDLE + + +struct lappenchat_server_options { + WSADATA wsa_data; + u_short port; + size_t threads; +}; + +int lappenchat_server +( + struct lappenchat_server_options, + HANDLE stop_event +); + +#endif diff --git a/service.c b/service.c new file mode 100644 index 0000000..6cfabc9 --- /dev/null +++ b/service.c @@ -0,0 +1,193 @@ +#include +#include "service.h" + +#include +#include "logmsg.h" +#include "error.h" +#include "common.h" +#include "server.h" + + +typedef struct +{ + SERVICE_STATUS status; + SERVICE_STATUS_HANDLE status_handle; + HANDLE stop_event; +} ServiceStuff; + +DWORD WINAPI ServiceCtrlHandler +( + DWORD request, + DWORD event_type, + LPVOID event_data, + LPVOID data +) +{ + ServiceStuff * const service_stuff = (ServiceStuff *)data; + + switch ( request ) + { + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + service_stuff->status.dwCurrentState = SERVICE_STOP_PENDING; + SetServiceStatus(service_stuff->status_handle, &service_stuff->status); + + logmsg("received stop request from the Service Control Manager"); + + if ( SetEvent(service_stuff->stop_event) ) + { + logmsg("stop event object triggered successfully"); + return NO_ERROR; + } + else + { + winapi_perror("couldn't trigger the stop event object"); + + logmsg("couldn't shut down gracefully"); + + service_stuff->status.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus(service_stuff->status_handle, &service_stuff->status); + + return NO_ERROR; + } + case SERVICE_CONTROL_INTERROGATE: + return NO_ERROR; + default: + return ERROR_CALL_NOT_IMPLEMENTED; + } +} + +VOID WINAPI +ServiceMain +( + int argc, + char * * argv +) +{ + ServiceStuff service_stuff = { + .status = { + .dwServiceType = SERVICE_WIN32, + .dwCurrentState = SERVICE_START_PENDING, + .dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN, + .dwWin32ExitCode = 1, + .dwServiceSpecificExitCode = 0, + .dwCheckPoint = 0, + .dwWaitHint = 0 + } + }; + + if ( service_stuff.status_handle = RegisterServiceCtrlHandlerEx("lappenchat-server", (LPHANDLER_FUNCTION_EX)ServiceCtrlHandler, &service_stuff) ) + { + SetServiceStatus(service_stuff.status_handle, &service_stuff.status); + + service_stuff.stop_event = CreateEvent(NULL, TRUE, FALSE, NULL); + if ( service_stuff.stop_event ) + { + char parameter = 0; + char * log_path = NULL; + struct lappenchat_server_options lcso = { 0 }; + + for ( char * * arg_cur = argv, * * const argv_end = argv+argc ; arg_cur != argv_end ; ++arg_cur ) + { + char * const arg = *arg_cur; + if ( parameter ) + { + switch ( parameter ) + { + case 'l': + log_path = arg; + break; + /* FIXME: strtol parses a sign ('+' or '-') as well. + * This should actually be prohibited here, as a "negative + * port" or a "negative number of threads" don't make + * sense. */ + case 'p': + /* Here, we should also check that the number + * provided doesn't exceed the maximum port #. */ + lcso.port = (u_short)strtol(arg, NULL, 10); + break; + case 't': + lcso.threads = strtol(arg, NULL, 10); + break; + } + parameter = 0; + } + else + if ( *arg == '-' ) + parameter = arg[1]; + } + + if ( !lcso.port ) + lcso.port = 3144; + + if ( !lcso.threads ) + lcso.threads = get_proc_n(); + + /* FIXME: we should report SERVICE_RUNNING only when (if) everything + * has been set up properly. The problem is that there is still setup + * work to do on the server side, so it would have to be reported from + * there, and that would make it dependent on being a Windows service. + * A way to avoid that would be to pass a callback, but that doesn't + * look pretty... */ + service_stuff.status.dwCurrentState = SERVICE_RUNNING; + SetServiceStatus(service_stuff.status_handle, &service_stuff.status); + + int rv; + if ( log_path ) + { + logout = fopen(log_path, "wb"); + if ( logout ) + { + logmsg("successfully opened log file"); + + rv = start_server(lcso, service_stuff.stop_event); + + fclose(logout); + } + else + rv = 0; + } + else + rv = start_server(lcso, service_stuff.stop_event); + + CloseHandle(service_stuff.stop_event); + + service_stuff.status.dwWin32ExitCode = !rv; + } + else + winapi_perror("couldn't create stop event object"); + } + else + winapi_perror("couldn't register service control handler"); + + service_stuff.status.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus(service_stuff.status_handle, &service_stuff.status); +} + +int main(void) +{ + SERVICE_TABLE_ENTRY service_table[] = { + { + .lpServiceName = "lappenchat-server", + .lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain + }, + { + NULL, + NULL + } + }; + + /* Use stderr until the log file is opened */ + logout = stderr; + + if ( StartServiceCtrlDispatcher(service_table) ) + return EXIT_SUCCESS; + else + { + winapi_perror("error: couldn't connect main thread to the service control manager"); + logmsg("hint: launch the server as a service instead of as a mere program"); + logmsg("hint: create the service with SC.EXE"); + logmsg("hint: start the service using NET.EXE"); + return EXIT_FAILURE; + } +} diff --git a/service.h b/service.h new file mode 100644 index 0000000..e1d4b96 --- /dev/null +++ b/service.h @@ -0,0 +1,8 @@ +#include + + +VOID WINAPI ServiceMain +( + int argc, + char * * argv +); diff --git a/toolchains.tup/gnu.tup b/toolchains.tup/gnu.tup new file mode 100644 index 0000000..59c400d --- /dev/null +++ b/toolchains.tup/gnu.tup @@ -0,0 +1,20 @@ +ifdef CC + CC=@(CC) +else + CC=gcc +endif + +ifdef LIBS_COMMAND + LIBS_COMMAND=@(LIBS_COMMAND) +else + LIBS_COMMAND=-lws2_32 +endif + +ifdef LIBS_SERVICE + LIBS_SERVICE=@(LIBS_SERVICE) +else + LIBS_SERVICE=-ladvapi32 -lws2_32 +endif + +!cc = |> $(CC) -c @(CFLAGS) -o %o %f |> %B.o +!ld = |> $(CC) @(LDFLAGS) -o %o %f $(LIBS) |> diff --git a/toolchains.tup/msvc.tup b/toolchains.tup/msvc.tup new file mode 100644 index 0000000..7c939f8 --- /dev/null +++ b/toolchains.tup/msvc.tup @@ -0,0 +1,38 @@ +ifdef CL + CL=@(CL) +else + CL=cl.exe +endif + +ifdef CLFLAGS + CLFLAGS=@(CLFLAGS) +else + CLFLAGS=/nologo /W4 /wd4057 /wd4100 /wd4204 /wd4244 /wd4706 /wd4996 /WX /MT /Ox /Gs- /GF /GL /Gw /Gy +endif + +ifdef LDFLAGS + LDFLAGS=@(LDFLAGS) +else + LDFLAGS=/nologo /W4 /wd4057 /wd4100 /wd4204 /wd4244 /wd4706 /wd4996 /WX +endif + +ifdef LINKFLAGS + LINKFLAGS=@(LINKFLAGS) +else + LINKFLAGS=/LTCG /OPT:REF,ICF +endif + +ifdef LIBS_COMMAND + LIBS_COMMAND=@(LIBS_COMMAND) +else + LIBS_COMMAND=ws2_32.lib +endif + +ifdef LIBS_SERVICE + LIBS_SERVICE=@(LIBS_SERVICE) +else + LIBS_SERVICE=advapi32.lib ws2_32.lib +endif + +!cc = |> $(CL) /c $(CLFLAGS) /Fo%o %f |> %B.obj +!ld = |> $(CL) $(LDFLAGS) /Fe%o %f $(LIBS) /link $(LINKFLAGS) |>