From 5a85e81685300e2299dabfeb25d513b99df471be Mon Sep 17 00:00:00 2001 From: Paweł Redman Date: Fri, 6 Sep 2013 22:40:51 +0200 Subject: Initial commit --- src/master/Makefile | 64 ++++ src/master/common.h | 91 ++++++ src/master/master.c | 877 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/master/messages.c | 574 +++++++++++++++++++++++++++++++++ src/master/messages.h | 35 ++ src/master/servers.c | 666 ++++++++++++++++++++++++++++++++++++++ src/master/servers.h | 106 ++++++ src/master/stats.c | 136 ++++++++ 8 files changed, 2549 insertions(+) create mode 100644 src/master/Makefile create mode 100644 src/master/common.h create mode 100644 src/master/master.c create mode 100644 src/master/messages.c create mode 100644 src/master/messages.h create mode 100644 src/master/servers.c create mode 100644 src/master/servers.h create mode 100644 src/master/stats.c (limited to 'src/master') diff --git a/src/master/Makefile b/src/master/Makefile new file mode 100644 index 0000000..a3a3f87 --- /dev/null +++ b/src/master/Makefile @@ -0,0 +1,64 @@ +BD_DEBUG=debug-$(PLATFORM)-$(ARCH) +BD_RELEASE=release-$(PLATFORM)-$(ARCH) + +ifndef PLATFORM + PLATFORM=$(shell uname|sed -e s/_.*//|tr '[:upper:]' '[:lower:]') +endif +ifndef ARCH + ARCH=$(shell uname -m | sed -e s/i.86/x86/) +endif + +ifeq ($(PLATFORM),mingw32) + BINEXT=.exe + RELEASE_LDFLAGS=-lwsock32 + DEBUG_LDFLAGS=-lwsock32 + RM=rm -f + MKDIR=mkdir +else + BINEXT= + RELEASE_LDFLAGS=-ltdb + DEBUG_LDFLAGS=-ltdb + RM=rm -f + MKDIR=mkdir +endif + +CC=gcc +RELEASE_CFLAGS=-Wall -O2 +DEBUG_CFLAGS=-g +OBJECTS= \ + $(BD)/master.o \ + $(BD)/messages.o \ + $(BD)/stats.o \ + $(BD)/servers.o + +release: makedirs + $(MAKE) $(BD_RELEASE)/tremmaster BD=$(BD_RELEASE) \ + CFLAGS="$(CFLAGS) $(RELEASE_CFLAGS)" LDFLAGS="$(LDFLAGS) $(RELEASE_LDFLAGS)" + +debug: makedirs + $(MAKE) $(BD_DEBUG)/tremmaster BD=$(BD_DEBUG) \ + CFLAGS="$(CFLAGS) $(DEBUG_CFLAGS)" LDFLAGS="$(LDFLAGS) $(DEBUG_LDFLAGS)" + +all: release debug + +$(BD)/%.o: %.c + $(CC) $(CFLAGS) -o $@ -c $< + +$(BD)/tremmaster: $(OBJECTS) + $(CC) -o $@ $(OBJECTS) $(LDFLAGS) + +clean: + -$(RM) $(BD_DEBUG)/* + -$(RM) $(BD_RELEASE)/* + +makedirs: + @if [ ! -d $(BD_RELEASE) ];then $(MKDIR) $(BD_RELEASE);fi + @if [ ! -d $(BD_DEBUG) ];then $(MKDIR) $(BD_DEBUG);fi + +.PHONY: all clean release debug makedirs + +# for f in *.c ; do cpp -MM ${f} -MT\$\(BD\)/${f%.c}.o ; done +$(BD)/master.o: master.c common.h messages.h servers.h +$(BD)/messages.o: messages.c common.h messages.h servers.h +$(BD)/servers.o: servers.c common.h servers.h +$(BD)/stats.o: stats.c common.h diff --git a/src/master/common.h b/src/master/common.h new file mode 100644 index 0000000..8c5c3b6 --- /dev/null +++ b/src/master/common.h @@ -0,0 +1,91 @@ +/* + common.h + + Common header file for dpmaster + + Copyright (C) 2004-2005 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#ifndef _COMMON_H_ +#define _COMMON_H_ + + +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +# include +#else +# include +# include +# include +# include +#endif + + +// ---------- Types ---------- // + +// A few basic types +typedef enum {qfalse, qtrue} qboolean; +typedef unsigned char qbyte; + +// The various messages levels +typedef enum +{ + MSG_NOPRINT, // used by "max_msg_level" (= no printings) + MSG_ERROR, // errors + MSG_WARNING, // warnings + MSG_NORMAL, // standard messages + MSG_DEBUG // for debugging purpose +} msg_level_t; + + +// ---------- Public variables ---------- // + +// The master socket +extern int inSock; +extern int outSock; + +// The current time (updated every time we receive a packet) +extern time_t crt_time; + +// Maximum level for a message to be printed +extern msg_level_t max_msg_level; + +// Peer address. We rebuild it every time we receive a new packet +extern char peer_address [128]; + + +// ---------- Public functions ---------- // + +// Win32 uses a different name for some standard functions +#ifdef WIN32 +# define snprintf _snprintf +#endif + +// Print a message to screen, depending on its verbose level +int MsgPrint (msg_level_t msg_level, const char* format, ...); + +void RecordClientStat( const char *address, const char *version, const char *renderer ); +void RecordGameStat( const char *address, const char *dataText ); + +#endif // #ifndef _COMMON_H_ diff --git a/src/master/master.c b/src/master/master.c new file mode 100644 index 0000000..fd6e66f --- /dev/null +++ b/src/master/master.c @@ -0,0 +1,877 @@ +/* + master.c + + A master server for Tremulous + + Copyright (C) 2002-2005 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#include +#include +#include + +#ifndef WIN32 +# include +# include +#endif + +#include "common.h" +#include "messages.h" +#include "servers.h" + + +// ---------- Constants ---------- // + +// Version of dpmaster +#define VERSION "1.6" + +// Default master port +#define DEFAULT_MASTER_PORT 30710 + +// Maximum and minimum sizes for a valid packet +#define MAX_PACKET_SIZE 2048 +#define MIN_PACKET_SIZE 5 + +#ifndef WIN32 +// Default path we use for chroot +# define DEFAULT_JAIL_PATH "/var/empty/" + +// User we use by default for dropping super-user privileges +# define DEFAULT_LOW_PRIV_USER "nobody" +#endif + + +// ---------- Types ---------- // + +#ifdef WIN32 +typedef int socklen_t; +#endif + + +// ---------- Private variables ---------- // + +// The port we use +static unsigned short master_port = DEFAULT_MASTER_PORT; + +// Local address we listen on, if any +static const char* listen_name = NULL; +static struct in_addr listen_addr; + +#ifndef WIN32 +// On UNIX systems, we can run as a daemon +static qboolean daemon_mode = qfalse; + +// Path we use for chroot +static const char* jail_path = DEFAULT_JAIL_PATH; + +// Low privileges user +static const char* low_priv_user = DEFAULT_LOW_PRIV_USER; +#endif + + +// ---------- Public variables ---------- // + +// The master socket +int inSock = -1; +int outSock = -1; + +// The current time (updated every time we receive a packet) +time_t crt_time; + +// Maximum level for a message to be printed +msg_level_t max_msg_level = MSG_NORMAL; + +// Peer address. We rebuild it every time we receive a new packet +char peer_address [128]; + + +// ---------- Private functions ---------- // + +/* +==================== +PrintPacket + +Print the contents of a packet on stdout +==================== +*/ +static void PrintPacket (const char* packet, size_t length) +{ + size_t i; + + // Exceptionally, we use MSG_NOPRINT here because if the function is + // called, the user probably wants this text to be displayed + // whatever the maximum message level is. + MsgPrint (MSG_NOPRINT, "\""); + + for (i = 0; i < length; i++) + { + char c = packet[i]; + if (c == '\\') + MsgPrint (MSG_NOPRINT, "\\\\"); + else if (c == '\"') + MsgPrint (MSG_NOPRINT, "\""); + else if (c >= 32 && (qbyte)c <= 127) + MsgPrint (MSG_NOPRINT, "%c", c); + else + MsgPrint (MSG_NOPRINT, "\\x%02X", c); + } + + MsgPrint (MSG_NOPRINT, "\" (%u bytes)\n", length); +} + + +/* +==================== +SysInit + +System dependent initializations +==================== +*/ +static qboolean SysInit (void) +{ +#ifdef WIN32 + WSADATA winsockdata; + + if (WSAStartup (MAKEWORD (1, 1), &winsockdata)) + { + MsgPrint (MSG_ERROR, "ERROR: can't initialize winsocks\n"); + return qfalse; + } +#endif + + return qtrue; +} + + +/* +==================== +UnsecureInit + +System independent initializations, called BEFORE the security initializations. +We need this intermediate step because DNS requests may not be able to resolve +after the security initializations, due to chroot. +==================== +*/ +static qboolean UnsecureInit (void) +{ + // Resolve the address mapping list + if (! Sv_ResolveAddressMappings ()) + return qfalse; + + // Resolve the listen address if one was specified + if (listen_name != NULL) + { + struct hostent* itf; + + itf = gethostbyname (listen_name); + if (itf == NULL) + { + MsgPrint (MSG_ERROR, "ERROR: can't resolve %s\n", listen_name); + return qfalse; + } + if (itf->h_addrtype != AF_INET) + { + MsgPrint (MSG_ERROR, "ERROR: %s is not an IPv4 address\n", + listen_name); + return qfalse; + } + + memcpy (&listen_addr.s_addr, itf->h_addr, + sizeof (listen_addr.s_addr)); + } + + return qtrue; +} + + +/* +==================== +SecInit + +Security initializations (system dependent) +==================== +*/ +static qboolean SecInit (void) +{ +#ifndef WIN32 + // Should we run as a daemon? + if (daemon_mode && daemon (0, 0)) + { + MsgPrint (MSG_NOPRINT, "ERROR: daemonization failed (%s)\n", + strerror (errno)); + return qfalse; + } + + // UNIX allows us to be completely paranoid, so let's go for it + if (geteuid () == 0) + { + struct passwd* pw; + + MsgPrint (MSG_WARNING, + "WARNING: running with super-user privileges\n"); + + // We must get the account infos before the calls to chroot and chdir + pw = getpwnam (low_priv_user); + if (pw == NULL) + { + MsgPrint (MSG_ERROR, "ERROR: can't get user \"%s\" properties\n", + low_priv_user); + return qfalse; + } + + // Chroot ourself + MsgPrint (MSG_NORMAL, " - chrooting myself to %s... ", jail_path); + if (chroot (jail_path) || chdir ("/")) + { + MsgPrint (MSG_ERROR, "FAILED (%s)\n", strerror (errno)); + return qfalse; + } + MsgPrint (MSG_NORMAL, "succeeded\n"); + + // Switch to lower privileges + MsgPrint (MSG_NORMAL, " - switching to user \"%s\" privileges... ", + low_priv_user); + if (setgid (pw->pw_gid) || setuid (pw->pw_uid)) + { + MsgPrint (MSG_ERROR, "FAILED (%s)\n", strerror (errno)); + return qfalse; + } + MsgPrint (MSG_NORMAL, "succeeded (UID: %u, GID: %u)\n", + pw->pw_uid, pw->pw_gid); + + MsgPrint (MSG_NORMAL, "\n"); + } +#endif + + return qtrue; +} + + +/* +==================== +ParseCommandLine + +Parse the options passed by the command line +==================== +*/ +static qboolean ParseCommandLine (int argc, const char* argv []) +{ + int ind = 1; + unsigned int vlevel = max_msg_level; + qboolean valid_options = qtrue; + + while (ind < argc && valid_options) + { + // If it doesn't even look like an option, why bother? + if (argv[ind][0] != '-') + valid_options = qfalse; + + else switch (argv[ind][1]) + { +#ifndef WIN32 + // Daemon mode + case 'D': + daemon_mode = qtrue; + break; +#endif + + // Help + case 'h': + valid_options = qfalse; + break; + + // Hash size + case 'H': + ind++; + if (ind < argc) + valid_options = Sv_SetHashSize (atoi (argv[ind])); + else + valid_options = qfalse; + break; + +#ifndef WIN32 + // Jail path + case 'j': + ind++; + if (ind < argc) + jail_path = argv[ind]; + else + valid_options = qfalse; + break; +#endif + + // Listen address + case 'l': + ind++; + if (ind >= argc || argv[ind][0] == '\0') + valid_options = qfalse; + else + listen_name = argv[ind]; + break; + + // Address mapping + case 'm': + ind++; + if (ind < argc) + valid_options = Sv_AddAddressMapping (argv[ind]); + else + valid_options = qfalse; + break; + + // Maximum number of servers + case 'n': + ind++; + if (ind < argc) + valid_options = Sv_SetMaxNbServers (atoi (argv[ind])); + else + valid_options = qfalse; + break; + + // Port number + case 'p': + { + unsigned short port_num = 0; + ind++; + if (ind < argc) + port_num = atoi (argv[ind]); + if (!port_num) + valid_options = qfalse; + else + master_port = port_num; + break; + } + +#ifndef WIN32 + // Low privileges user + case 'u': + ind++; + if (ind < argc) + low_priv_user = argv[ind]; + else + valid_options = qfalse; + break; +#endif + + // Verbose level + case 'v': + // If a verbose level has been specified + if (ind + 1 < argc && argv[ind + 1][0] != '-') + { + ind++; + vlevel = atoi (argv[ind]); + if (vlevel > MSG_DEBUG) + valid_options = qfalse; + } + else + vlevel = MSG_DEBUG; + break; + + default: + valid_options = qfalse; + } + + ind++; + } + + // If the command line is OK, we can set the verbose level now + if (valid_options) + { +#ifndef WIN32 + // If we run as a daemon, don't bother printing anything + if (daemon_mode) + max_msg_level = MSG_NOPRINT; + else +#endif + max_msg_level = vlevel; + } + + return valid_options; +} + + +/* +==================== +PrintHelp + +Print the command line syntax and the available options +==================== +*/ +static void PrintHelp (void) +{ + MsgPrint (MSG_ERROR, + "Syntax: dpmaster [options]\n" + "Available options are:\n" +#ifndef WIN32 + " -D : run as a daemon\n" +#endif + " -h : this help\n" + " -H : hash size in bits, up to %u (default: %u)\n" +#ifndef WIN32 + " -j : use as chroot path (default: %s)\n" + " only available when running with super-user privileges\n" +#endif + " -l
: listen on local address
\n" + " -m = : map address to when sending it to clients\n" + " addresses can contain a port number (ex: myaddr.net:1234)\n" + " -n : maximum number of servers recorded (default: %u)\n" + " -p : use port (default: %u)\n" +#ifndef WIN32 + " -u : use privileges (default: %s)\n" + " only available when running with super-user privileges\n" +#endif + " -v [verbose_lvl] : verbose level, up to %u (default: %u; no value means max)\n" + "\n", + MAX_HASH_SIZE, DEFAULT_HASH_SIZE, +#ifndef WIN32 + DEFAULT_JAIL_PATH, +#endif + DEFAULT_MAX_NB_SERVERS, + DEFAULT_MASTER_PORT, +#ifndef WIN32 + DEFAULT_LOW_PRIV_USER, +#endif + MSG_DEBUG, MSG_NORMAL); +} + + +/* +==================== +SecureInit + +System independent initializations, called AFTER the security initializations +==================== +*/ +static qboolean SecureInit (void) +{ + struct sockaddr_in address; + + // Init the time and the random seed + crt_time = time (NULL); + srand (crt_time); + + // Initialize the server list and hash table + if (!Sv_Init ()) + return qfalse; + + // Open the socket + inSock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (inSock < 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket creation failed (%s)\n", + strerror (errno)); + return qfalse; + } + + outSock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (outSock < 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket creation failed (%s)\n", + strerror (errno)); + return qfalse; + } + + // Bind it to the master port + memset (&address, 0, sizeof (address)); + address.sin_family = AF_INET; + if (listen_name != NULL) + { + MsgPrint (MSG_NORMAL, "Listening on address %s (%s)\n", + listen_name, inet_ntoa (listen_addr)); + address.sin_addr.s_addr = listen_addr.s_addr; + } + else + address.sin_addr.s_addr = htonl (INADDR_ANY); + + address.sin_port = htons (master_port); + if (bind (inSock, (struct sockaddr*)&address, sizeof (address)) != 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket binding failed (%s)\n", + strerror (errno)); +#ifdef WIN32 + closesocket (inSock); +#else + close (inSock); +#endif + return qfalse; + } + MsgPrint (MSG_NORMAL, "Listening on UDP port %hu\n", + ntohs (address.sin_port)); + + // Deliberately use a different port for outgoing traffic in order + // to confuse NAT UDP "connection" tracking and thus delist servers + // hidden by NAT + address.sin_port = htons (master_port+1); + if (bind (outSock, (struct sockaddr*)&address, sizeof (address)) != 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket binding failed (%s)\n", + strerror (errno)); +#ifdef WIN32 + closesocket (outSock); +#else + close (outSock); +#endif + return qfalse; + } + + return qtrue; +} + +static qboolean exitNow = qfalse; + +/* +=============== +cleanUp + +Clean up +=============== +*/ +static void cleanUp( int signal ) +{ + MsgPrint( MSG_NORMAL, "Caught signal %d, exiting...\n", signal ); + + exitNow = qtrue; +} + +#define ADDRESS_LENGTH 16 +static const char *ignoreFile = "ignore.txt"; + +typedef struct +{ + char address[ ADDRESS_LENGTH ]; // Dotted quad +} ignoreAddress_t; + +#define PARSE_INTERVAL 10 // seconds + +static time_t lastParseTime = 0; +static int numIgnoreAddresses = 0; +static ignoreAddress_t *ignoreAddresses = NULL; + +/* +==================== +parseIgnoreAddress +==================== +*/ +static qboolean parseIgnoreAddress( void ) +{ + int numAllocIgnoreAddresses = 1; + FILE *f = NULL; + int i; + + // Only reparse periodically + if( crt_time - lastParseTime < PARSE_INTERVAL ) + return qtrue; + + lastParseTime = time( NULL ); + + // Free existing list + if( ignoreAddresses != NULL ) + { + free( ignoreAddresses ); + ignoreAddresses = NULL; + } + + numIgnoreAddresses = 0; + ignoreAddresses = malloc( sizeof( ignoreAddress_t ) * numAllocIgnoreAddresses ); + + // Alloc failed, fail parsing + if( ignoreAddresses == NULL ) + return qfalse; + + f = fopen( ignoreFile, "r" ); + + if( !f ) + { + free( ignoreAddresses ); + ignoreAddresses = NULL; + return qfalse; + } + + while( !feof( f ) ) + { + char c; + char buffer[ ADDRESS_LENGTH ]; + + i = 0; + + // Skip whitespace + do + { + c = fgetc( f ); + } + while( c != EOF && isspace( c ) ); + + if( c != EOF ) + { + do + { + if( i >= ADDRESS_LENGTH ) + { + buffer[ i - 1 ] = '\0'; + break; + } + + buffer[ i ] = c; + + if( isspace( c ) ) + { + buffer[ i ] = '\0'; + break; + } + + i++; + } while( ( c = fgetc( f ) ) != EOF ); + + strcpy( ignoreAddresses[ numIgnoreAddresses ].address, buffer ); + + numIgnoreAddresses++; + + // Make list bigger + if( numIgnoreAddresses >= numAllocIgnoreAddresses ) + { + ignoreAddress_t *new; + + numAllocIgnoreAddresses *= 2; + new = realloc( ignoreAddresses, + sizeof( ignoreAddress_t ) * numAllocIgnoreAddresses ); + + // Alloc failed, fail parsing + if( new == NULL ) + { + fclose( f ); + free( ignoreAddresses ); + ignoreAddresses = NULL; + return qfalse; + } + + ignoreAddresses = new; + } + } + } + + fclose( f ); + + return qtrue; +} + +/* +==================== +ignoreAddress + +Check whether or not to ignore a specific address +==================== +*/ +static qboolean ignoreAddress( const char *address ) +{ + int i; + + if( !parseIgnoreAddress( ) ) + { + // Couldn't parse, allow the address + return qfalse; + } + + for( i = 0; i < numIgnoreAddresses; i++ ) + { + if( strcmp( address, ignoreAddresses[ i ].address ) == 0 ) + break; + } + + // It matched + if( i < numIgnoreAddresses ) + return qtrue; + + return qfalse; +} + +/* +==================== +max + +Maximum of two ints +==================== +*/ +static inline int max( int a, int b ) +{ + return a > b ? a : b; +} + +/* +==================== +main + +Main function +==================== +*/ +int main (int argc, const char* argv []) +{ + struct sockaddr_in address; + socklen_t addrlen; + int nb_bytes; + int sock; + char packet [MAX_PACKET_SIZE + 1]; // "+ 1" because we append a '\0' + qboolean valid_options; + fd_set rfds; + struct timeval tv; + + + signal( SIGINT, cleanUp ); + signal( SIGTERM, cleanUp ); + + // Get the options from the command line + valid_options = ParseCommandLine (argc, argv); + + MsgPrint (MSG_NORMAL, + "tremmaster (version " VERSION " " __DATE__ " " __TIME__ ")\n" ); + + // If there was a mistake in the command line, print the help and exit + if (!valid_options) + { + PrintHelp (); + return EXIT_FAILURE; + } + + // Initializations + if (!SysInit () || !UnsecureInit () || !SecInit () || !SecureInit ()) + return EXIT_FAILURE; + MsgPrint (MSG_NORMAL, "\n"); + + // Until the end of times... + while( !exitNow ) + { + FD_ZERO( &rfds ); + FD_SET( inSock, &rfds ); + FD_SET( outSock, &rfds ); + tv.tv_sec = tv.tv_usec = 0; + + // Check for new data every 100ms + if( select( max( inSock, outSock ) + 1, &rfds, NULL, NULL, &tv ) <= 0 ) + { +#ifdef _WIN32 + Sleep( 100 ); +#else + usleep( 100000 ); +#endif + continue; + } + + if( FD_ISSET( inSock, &rfds ) ) + sock = inSock; + else if( FD_ISSET( outSock, &rfds ) ) + sock = outSock; + else + continue; + + // Get the next valid message + addrlen = sizeof (address); + nb_bytes = recvfrom (sock, packet, sizeof (packet) - 1, 0, + (struct sockaddr*)&address, &addrlen); + if (nb_bytes <= 0) + { + MsgPrint (MSG_WARNING, + "WARNING: \"recvfrom\" returned %d\n", nb_bytes); + continue; + } + + // If we may have to print something, rebuild the peer address buffer + if (max_msg_level != MSG_NOPRINT) + snprintf (peer_address, sizeof (peer_address), "%s:%hu", + inet_ntoa (address.sin_addr), ntohs (address.sin_port)); + + // Ignore abusers + if( ignoreAddress( inet_ntoa( address.sin_addr ) ) ) + { + server_t* abuser = Sv_GetByAddr( &address, qfalse ); + if( abuser != NULL ) + { + abuser->timeout = crt_time - 1; + Sv_GetByAddr( &address, qfalse ); + MsgPrint( MSG_WARNING, "WARNING: removing abuser %s\n", peer_address ); + } + continue; + } + + // We print the packet contents if necessary + // TODO: print the current time here + if (max_msg_level >= MSG_DEBUG) + { + MsgPrint (MSG_DEBUG, "New packet received from %s: ", + peer_address); + PrintPacket (packet, nb_bytes); + } + + // A few sanity checks + if (nb_bytes < MIN_PACKET_SIZE) + { + MsgPrint (MSG_WARNING, + "WARNING: rejected packet from %s (size = %d bytes)\n", + peer_address, nb_bytes); + continue; + } + if (*((unsigned int*)packet) != 0xFFFFFFFF) + { + MsgPrint (MSG_WARNING, + "WARNING: rejected packet from %s (invalid header)\n", + peer_address); + continue; + } + if( ntohs( address.sin_port ) < 1024 ) + { + MsgPrint (MSG_WARNING, + "WARNING: rejected packet from %s (source port = 0)\n", + peer_address); + continue; + } + + // Append a '\0' to make the parsing easier and update the current time + packet[nb_bytes] = '\0'; + crt_time = time (NULL); + + // Call HandleMessage with the remaining contents + HandleMessage (packet + 4, nb_bytes - 4, &address); + } + + return 0; +} + + +// ---------- Public functions ---------- // + +/* +==================== +MsgPrint + +Print a message to screen, depending on its verbose level +==================== +*/ +int MsgPrint (msg_level_t msg_level, const char* format, ...) +{ + va_list args; + int result; + + // If the message level is above the maximum level, don't print it + if (msg_level > max_msg_level) + return 0; + + va_start (args, format); + result = vprintf (format, args); + va_end (args); + + fflush (stdout); + + return result; +} diff --git a/src/master/messages.c b/src/master/messages.c new file mode 100644 index 0000000..a86e795 --- /dev/null +++ b/src/master/messages.c @@ -0,0 +1,574 @@ +/* + messages.c + + Message management for tremmaster + + Copyright (C) 2004 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#include "common.h" +#include "messages.h" +#include "servers.h" + + +// ---------- Constants ---------- // + +// Timeouts (in secondes) +#define TIMEOUT_HEARTBEAT 2 +#define TIMEOUT_INFORESPONSE (15 * 60) + +// Period of validity for a challenge string (in secondes) +#define TIMEOUT_CHALLENGE 2 + +// Maximum size of a reponse packet +#define MAX_PACKET_SIZE 1400 + + +// Types of messages (with samples): + +// "heartbeat Tremulous\n" +#define S2M_HEARTBEAT "heartbeat" + +// "gamestat " +#define S2M_GAMESTAT "gamestat" + +// "getinfo A_Challenge" +#define M2S_GETINFO "getinfo" + +// "infoResponse\n\\pure\\1\\..." +#define S2M_INFORESPONSE "infoResponse\x0A" + +// "getservers 67 empty full" +#define C2M_GETSERVERS "getservers " + +// "getserversResponse\\...(6 bytes)...\\...(6 bytes)...\\EOT\0\0\0" +#define M2C_GETSERVERSREPONSE "getserversResponse" + +#define C2M_GETMOTD "getmotd" +#define M2C_MOTD "motd " + + +// ---------- Private functions ---------- // + +/* +==================== +SearchInfostring + +Search an infostring for the value of a key +==================== +*/ +static char* SearchInfostring (const char* infostring, const char* key) +{ + static char value [256]; + char crt_key [256]; + size_t value_ind, key_ind; + char c; + + if (*infostring++ != '\\') + return NULL; + + value_ind = 0; + for (;;) + { + key_ind = 0; + + // Get the key name + for (;;) + { + c = *infostring++; + + if (c == '\0') + return NULL; + if (c == '\\' || key_ind == sizeof (crt_key) - 1) + { + crt_key[key_ind] = '\0'; + break; + } + + crt_key[key_ind++] = c; + } + + // If it's the key we are looking for, save it in "value" + if (!strcmp (crt_key, key)) + { + for (;;) + { + c = *infostring++; + + if (c == '\0' || c == '\\' || value_ind == sizeof (value) - 1) + { + value[value_ind] = '\0'; + return value; + } + + value[value_ind++] = c; + } + } + + // Else, skip the value + for (;;) + { + c = *infostring++; + + if (c == '\0') + return NULL; + if (c == '\\') + break; + } + } +} + + +/* +==================== +BuildChallenge + +Build a challenge string for a "getinfo" message +==================== +*/ +static const char* BuildChallenge (void) +{ + static char challenge [CHALLENGE_MAX_LENGTH]; + size_t ind; + size_t length = CHALLENGE_MIN_LENGTH - 1; // We start at the minimum size + + // ... then we add a random number of characters + length += rand () % (CHALLENGE_MAX_LENGTH - CHALLENGE_MIN_LENGTH + 1); + + for (ind = 0; ind < length; ind++) + { + char c; + do + { + c = 33 + rand () % (126 - 33 + 1); // -> c = 33..126 + } while (c == '\\' || c == ';' || c == '"' || c == '%' || c == '/'); + + challenge[ind] = c; + } + + challenge[length] = '\0'; + return challenge; +} + + +/* +==================== +SendGetInfo + +Send a "getinfo" message to a server +==================== +*/ +static void SendGetInfo (server_t* server) +{ + char msg [64] = "\xFF\xFF\xFF\xFF" M2S_GETINFO " "; + + if (!server->challenge_timeout || server->challenge_timeout < crt_time) + { + strncpy (server->challenge, BuildChallenge (), + sizeof (server->challenge) - 1); + server->challenge_timeout = crt_time + TIMEOUT_CHALLENGE; + } + + strncat (msg, server->challenge, sizeof (msg) - strlen (msg) - 1); + sendto (outSock, msg, strlen (msg), 0, + (const struct sockaddr*)&server->address, + sizeof (server->address)); + + MsgPrint (MSG_DEBUG, "%s <--- getinfo with challenge \"%s\"\n", + peer_address, server->challenge); +} + + +/* +==================== +HandleGetServers + +Parse getservers requests and send the appropriate response +==================== +*/ +static void HandleGetServers (const char* msg, const struct sockaddr_in* addr) +{ + const char* packetheader = "\xFF\xFF\xFF\xFF" M2C_GETSERVERSREPONSE "\\"; + const size_t headersize = strlen (packetheader); + char packet [MAX_PACKET_SIZE]; + size_t packetind; + server_t* sv; + unsigned int protocol; + unsigned int sv_addr; + unsigned short sv_port; + qboolean no_empty; + qboolean no_full; + unsigned int numServers = 0; + + // Check if there's a name before the protocol number + // In this case, the message comes from a DarkPlaces-compatible client + protocol = atoi (msg); + + MsgPrint (MSG_NORMAL, "%s ---> getservers( protocol version %d )\n", + peer_address, protocol ); + + no_empty = (strstr (msg, "empty") == NULL); + no_full = (strstr (msg, "full") == NULL); + + // Initialize the packet contents with the header + packetind = headersize; + memcpy(packet, packetheader, headersize); + + // Add every relevant server + for (sv = Sv_GetFirst (); /* see below */; sv = Sv_GetNext ()) + { + // If we're done, or if the packet is full, send the packet + if (sv == NULL || packetind > sizeof (packet) - (7 + 6)) + { + // End Of Transmission + packet[packetind ] = 'E'; + packet[packetind + 1] = 'O'; + packet[packetind + 2] = 'T'; + packet[packetind + 3] = '\0'; + packet[packetind + 4] = '\0'; + packet[packetind + 5] = '\0'; + packetind += 6; + + // Send the packet to the client + sendto (inSock, packet, packetind, 0, (const struct sockaddr*)addr, + sizeof (*addr)); + + MsgPrint (MSG_DEBUG, "%s <--- getserversResponse (%u servers)\n", + peer_address, numServers); + + // If we're done + if (sv == NULL) + return; + + // Reset the packet index (no need to change the header) + packetind = headersize; + } + + sv_addr = ntohl (sv->address.sin_addr.s_addr); + sv_port = ntohs (sv->address.sin_port); + + // Extra debugging info + if (max_msg_level >= MSG_DEBUG) + { + MsgPrint (MSG_DEBUG, + "Comparing server: IP:\"%u.%u.%u.%u:%hu\", p:%u, c:%hu\n", + sv_addr >> 24, (sv_addr >> 16) & 0xFF, + (sv_addr >> 8) & 0xFF, sv_addr & 0xFF, + sv_port, sv->protocol, sv->nbclients ); + + if (sv->protocol != protocol) + MsgPrint (MSG_DEBUG, + "Reject: protocol %u != requested %u\n", + sv->protocol, protocol); + if (sv->nbclients == 0 && no_empty) + MsgPrint (MSG_DEBUG, + "Reject: nbclients is %hu/%hu && no_empty\n", + sv->nbclients, sv->maxclients); + if (sv->nbclients == sv->maxclients && no_full) + MsgPrint (MSG_DEBUG, + "Reject: nbclients is %hu/%hu && no_full\n", + sv->nbclients, sv->maxclients); + } + + // Check protocol, options + if (sv->protocol != protocol || + (sv->nbclients == 0 && no_empty) || + (sv->nbclients == sv->maxclients && no_full)) + { + + // Skip it + continue; + } + + // Use the address mapping associated with the server, if any + if (sv->addrmap != NULL) + { + const addrmap_t* addrmap = sv->addrmap; + + sv_addr = ntohl (addrmap->to.sin_addr.s_addr); + if (addrmap->to.sin_port != 0) + sv_port = ntohs (addrmap->to.sin_port); + + MsgPrint (MSG_DEBUG, + "Server address mapped to %u.%u.%u.%u:%hu\n", + sv_addr >> 24, (sv_addr >> 16) & 0xFF, + (sv_addr >> 8) & 0xFF, sv_addr & 0xFF, + sv_port); + } + + // IP address + packet[packetind ] = sv_addr >> 24; + packet[packetind + 1] = (sv_addr >> 16) & 0xFF; + packet[packetind + 2] = (sv_addr >> 8) & 0xFF; + packet[packetind + 3] = sv_addr & 0xFF; + + // Port + packet[packetind + 4] = sv_port >> 8; + packet[packetind + 5] = sv_port & 0xFF; + + // Trailing '\' + packet[packetind + 6] = '\\'; + + MsgPrint (MSG_DEBUG, " - Sending server %u.%u.%u.%u:%hu\n", + (qbyte)packet[packetind ], (qbyte)packet[packetind + 1], + (qbyte)packet[packetind + 2], (qbyte)packet[packetind + 3], + sv_port); + + packetind += 7; + numServers++; + } +} + + +/* +==================== +HandleInfoResponse + +Parse infoResponse messages +==================== +*/ +static void HandleInfoResponse (server_t* server, const char* msg) +{ + char* value; + unsigned int new_protocol = 0, new_maxclients = 0; + + MsgPrint (MSG_DEBUG, "%s ---> infoResponse\n", peer_address); + + // Check the challenge + if (!server->challenge_timeout || server->challenge_timeout < crt_time) + { + MsgPrint (MSG_WARNING, + "WARNING: infoResponse with obsolete challenge from %s\n", + peer_address); + return; + } + value = SearchInfostring (msg, "challenge"); + if (!value || strcmp (value, server->challenge)) + { + MsgPrint (MSG_ERROR, "ERROR: invalid challenge from %s (%s)\n", + peer_address, value); + return; + } + // some people enjoy making it hard to find them + value = SearchInfostring (msg, "hostname"); + if (!value || !value[0]) + { + MsgPrint (MSG_ERROR, "ERROR: no hostname from %s\n", + peer_address, value); + return; + } + + // Check and save the values of "protocol" and "maxclients" + value = SearchInfostring (msg, "protocol"); + if (value) + new_protocol = atoi (value); + value = SearchInfostring (msg, "sv_maxclients"); + if (value) + new_maxclients = atoi (value); + if (!new_protocol || !new_maxclients) + { + MsgPrint (MSG_ERROR, + "ERROR: invalid infoResponse from %s (protocol: %d, maxclients: %d)\n", + peer_address, new_protocol, new_maxclients); + return; + } + server->protocol = new_protocol; + server->maxclients = new_maxclients; + + // Save some other useful values + value = SearchInfostring (msg, "clients"); + if (value) + server->nbclients = atoi (value); + + // Set a new timeout + server->timeout = crt_time + TIMEOUT_INFORESPONSE; +} + + +#define CHALLENGE_KEY "challenge\\" +#define MOTD_KEY "motd\\" + +/* +==================== +HandleGetMotd + +Parse getservers requests and send the appropriate response +==================== +*/ +static void HandleGetMotd( const char* msg, const struct sockaddr_in* addr ) +{ + const char *packetheader = "\xFF\xFF\xFF\xFF" M2C_MOTD "\""; + const size_t headersize = strlen (packetheader); + char packet[ MAX_PACKET_SIZE ]; + char challenge[ MAX_PACKET_SIZE ]; + const char *motd = ""; //FIXME + size_t packetind; + char *value; + char version[ 1024 ], renderer[ 1024 ]; + + MsgPrint( MSG_DEBUG, "%s ---> getmotd\n", peer_address ); + + value = SearchInfostring( msg, "challenge" ); + if( !value ) + { + MsgPrint( MSG_ERROR, "ERROR: invalid challenge from %s (%s)\n", + peer_address, value ); + return; + } + + strncpy( challenge, value, sizeof(challenge)-1 ); + challenge[sizeof(challenge)-1] = '\0'; + + value = SearchInfostring( msg, "renderer" ); + if( value ) + { + strncpy( renderer, value, sizeof(renderer)-1 ); + renderer[sizeof(renderer)-1] = '\0'; + MsgPrint( MSG_DEBUG, "%s is using renderer %s\n", peer_address, value ); + } + + value = SearchInfostring( msg, "version" ); + if( value ) + { + strncpy( version, value, sizeof(version)-1 ); + version[sizeof(version)-1] = '\0'; + MsgPrint( MSG_DEBUG, "%s is using version %s\n", peer_address, value ); + } + +#ifndef _WIN32 + RecordClientStat( peer_address, version, renderer ); +#endif + + // Initialize the packet contents with the header + packetind = headersize; + memcpy( packet, packetheader, headersize ); + + strncpy( &packet[ packetind ], CHALLENGE_KEY, MAX_PACKET_SIZE - packetind - 2 ); + packetind += strlen( CHALLENGE_KEY ); + + strncpy( &packet[ packetind ], challenge, MAX_PACKET_SIZE - packetind - 2 ); + packetind += strlen( challenge ); + packet[ packetind++ ] = '\\'; + + strncpy( &packet[ packetind ], MOTD_KEY, MAX_PACKET_SIZE - packetind - 2 ); + packetind += strlen( MOTD_KEY ); + + strncpy( &packet[ packetind ], motd, MAX_PACKET_SIZE - packetind - 2 ); + packetind += strlen( motd ); + + if (packetind > MAX_PACKET_SIZE - 2) + packetind = MAX_PACKET_SIZE - 2; + + packet[ packetind++ ] = '\"'; + packet[ packetind++ ] = '\0'; + + MsgPrint( MSG_DEBUG, "%s <--- motd\n", peer_address ); + + // Send the packet to the client + sendto( inSock, packet, packetind, 0, (const struct sockaddr*)addr, + sizeof( *addr ) ); +} + +/* +==================== +HandleGameStat +==================== +*/ +static void HandleGameStat( const char* msg, const struct sockaddr_in* addr ) +{ +#ifndef _WIN32 + RecordGameStat( peer_address, msg ); +#endif +} + +// ---------- Public functions ---------- // + +/* +==================== +HandleMessage + +Parse a packet to figure out what to do with it +==================== +*/ +void HandleMessage (const char* msg, size_t length, + const struct sockaddr_in* address) +{ + server_t* server; + + // If it's an heartbeat + if (!strncmp (S2M_HEARTBEAT, msg, strlen (S2M_HEARTBEAT))) + { + char gameId [64]; + + // Extract the game id + sscanf (msg + strlen (S2M_HEARTBEAT) + 1, "%63s", gameId); + MsgPrint (MSG_DEBUG, "%s ---> heartbeat (%s)\n", + peer_address, gameId); + + // Get the server in the list (add it to the list if necessary) + server = Sv_GetByAddr (address, qtrue); + if (server == NULL) + return; + + server->active = qtrue; + + // If we haven't yet received any infoResponse from this server, + // we let it some more time to contact us. After that, only + // infoResponse messages can update the timeout value. + if (!server->maxclients) + server->timeout = crt_time + TIMEOUT_HEARTBEAT; + + // Ask for some infos + SendGetInfo (server); + } + + // If it's an infoResponse message + else if (!strncmp (S2M_INFORESPONSE, msg, strlen (S2M_INFORESPONSE))) + { + server = Sv_GetByAddr (address, qfalse); + if (server == NULL) + return; + + HandleInfoResponse (server, msg + strlen (S2M_INFORESPONSE)); + } + + // If it's a getservers request + else if (!strncmp (C2M_GETSERVERS, msg, strlen (C2M_GETSERVERS))) + { + HandleGetServers (msg + strlen (C2M_GETSERVERS), address); + } + + // If it's a getmotd request + else if (!strncmp (C2M_GETMOTD, msg, strlen (C2M_GETMOTD))) + { + HandleGetMotd (msg + strlen (C2M_GETMOTD), address); + } + + // If it's a game statistic + else if( !strncmp( S2M_GAMESTAT, msg, strlen ( S2M_GAMESTAT ) ) ) + { + server = Sv_GetByAddr(address, qfalse); + if (server == NULL) + return; + if( crt_time - server->lastGameStat > MIN_GAMESTAT_DELAY ) + HandleGameStat( msg + strlen( S2M_GAMESTAT ), address ); + else + MsgPrint( MSG_NORMAL, "%s flooding GAMESTAT messages\n", peer_address ); + server->lastGameStat = crt_time; + } +} diff --git a/src/master/messages.h b/src/master/messages.h new file mode 100644 index 0000000..93b5271 --- /dev/null +++ b/src/master/messages.h @@ -0,0 +1,35 @@ +/* + messages.h + + Message management for dpmaster + + Copyright (C) 2004 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#ifndef _MESSAGES_H_ +#define _MESSAGES_H_ + + +// ---------- Public functions ---------- // + +// Parse a packet to figure out what to do with it +void HandleMessage (const char* msg, size_t length, + const struct sockaddr_in* address); + + +#endif // #ifndef _MESSAGES_H_ diff --git a/src/master/servers.c b/src/master/servers.c new file mode 100644 index 0000000..a8412a1 --- /dev/null +++ b/src/master/servers.c @@ -0,0 +1,666 @@ +/* + servers.c + + Server list and address mapping management for dpmaster + + Copyright (C) 2004 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#include "common.h" +#include "servers.h" + + +// ---------- Constants ---------- // + +// Address hash bitmask +#define HASH_BITMASK (hash_table_size - 1) + + +// ---------- Variables ---------- // + +// All server structures are allocated in one block in the "servers" array. +// Each used slot is also part of a linked list in "hash_table". A simple +// hash of the address and port of a server gives its index in the table. +static server_t* servers = NULL; +static size_t max_nb_servers = DEFAULT_MAX_NB_SERVERS; +static size_t nb_servers = 0; +static server_t** hash_table = NULL; +static size_t hash_table_size = (1 << DEFAULT_HASH_SIZE); + +// Last allocated entry in the "servers" array. +// Used as a start index for finding a free slot in "servers" more quickly +static unsigned int last_alloc; + +// Variables for Sv_GetFirst, Sv_GetNext and Sv_RemoveCurrentAndGetNext +static server_t* crt_server = NULL; +static server_t** prev_pointer = NULL; +static int crt_hash_ind = -1; + +// List of address mappings. They are sorted by "from" field (IP, then port) +static addrmap_t* addrmaps = NULL; + + +// ---------- Private functions ---------- // + +/* +==================== +Sv_AddressHash + +Compute the hash of a server address +==================== +*/ +static unsigned int Sv_AddressHash (const struct sockaddr_in* address) +{ + qbyte* addr = (qbyte*)&address->sin_addr.s_addr; + qbyte* port = (qbyte*)&address->sin_port; + qbyte hash; + + hash = addr[0] ^ addr[1] ^ addr[2] ^ addr[3] ^ port[0] ^ port[1]; + return hash & HASH_BITMASK; +} + + +/* +==================== +Sv_RemoveAndGetNextPtr + +Remove a server from the list and returns its "next" pointer +==================== +*/ +static server_t* Sv_RemoveAndGetNextPtr (server_t* sv, server_t** prev) +{ + nb_servers--; + MsgPrint (MSG_NORMAL, + "%s:%hu timed out; %u servers currently registered\n", + inet_ntoa (sv->address.sin_addr), ntohs (sv->address.sin_port), + nb_servers); + + // Mark this structure as "free" + sv->active = qfalse; + + *prev = sv->next; + return sv->next; +} + + +/* +==================== +Sv_ResolveAddr + +Resolve an internet address +name may include a port number, after a ':' +==================== +*/ +static qboolean Sv_ResolveAddr (const char* name, struct sockaddr_in* addr) +{ + char *namecpy, *port; + struct hostent* host; + + // Create a work copy + namecpy = strdup (name); + if (namecpy == NULL) + { + MsgPrint (MSG_ERROR, + "ERROR: can't allocate enough memory to resolve %s\n", + name); + return qfalse; + } + + // Find the port in the address + port = strchr (namecpy, ':'); + if (port != NULL) + *port++ = '\0'; + + // Resolve the address + host = gethostbyname (namecpy); + if (host == NULL) + { + MsgPrint (MSG_ERROR, "ERROR: can't resolve %s\n", namecpy); + free (namecpy); + return qfalse; + } + if (host->h_addrtype != AF_INET) + { + MsgPrint (MSG_ERROR, "ERROR: %s is not an IPv4 address\n", + namecpy); + free (namecpy); + return qfalse; + } + + // Build the structure + memset (addr, 0, sizeof (*addr)); + addr->sin_family = AF_INET; + memcpy (&addr->sin_addr.s_addr, host->h_addr, + sizeof (addr->sin_addr.s_addr)); + if (port != NULL) + addr->sin_port = htons ((unsigned short)atoi (port)); + + MsgPrint (MSG_DEBUG, "\"%s\" resolved to %s:%hu\n", + name, inet_ntoa (addr->sin_addr), ntohs (addr->sin_port)); + + free (namecpy); + return qtrue; +} + + +/* +==================== +Sv_InsertAddrmapIntoList + +Insert an addrmap structure to the addrmaps list +==================== +*/ +static void Sv_InsertAddrmapIntoList (addrmap_t* new_map) +{ + addrmap_t* addrmap = addrmaps; + addrmap_t** prev = &addrmaps; + + // Stop at the end of the list, or if the addresses become too high + while (addrmap != NULL && + addrmap->from.sin_addr.s_addr <= new_map->from.sin_addr.s_addr) + { + // If we found the right place + if (addrmap->from.sin_addr.s_addr == new_map->from.sin_addr.s_addr && + addrmap->from.sin_port >= new_map->from.sin_port) + { + // If a mapping is already recorded for this address + if (addrmap->from.sin_port == new_map->from.sin_port) + { + MsgPrint (MSG_WARNING, + "WARNING: Address %s:%hu has several mappings\n", + inet_ntoa (new_map->from.sin_addr), + ntohs (new_map->from.sin_port)); + + *prev = addrmap->next; + free (addrmap); + } + break; + } + + prev = &addrmap->next; + addrmap = addrmap->next; + } + + // Insert it + new_map->next = *prev; + *prev = new_map; + + MsgPrint (MSG_NORMAL, "Address \"%s\" mapped to \"%s\" (%s:%hu)\n", + new_map->from_string, new_map->to_string, + inet_ntoa (new_map->to.sin_addr), ntohs (new_map->to.sin_port)); +} + + +/* +==================== +Sv_GetAddrmap + +Look for an address mapping corresponding to addr +==================== +*/ +static const addrmap_t* Sv_GetAddrmap (const struct sockaddr_in* addr) +{ + const addrmap_t* addrmap = addrmaps; + const addrmap_t* found = NULL; + + // Stop at the end of the list, or if the addresses become too high + while (addrmap != NULL && + addrmap->from.sin_addr.s_addr <= addr->sin_addr.s_addr) + { + // If it's the right address + if (addrmap->from.sin_addr.s_addr == addr->sin_addr.s_addr) + { + // If the exact mapping isn't there + if (addrmap->from.sin_port > addr->sin_port) + return found; + + // If we found the exact address + if (addrmap->from.sin_port == addr->sin_port) + return addrmap; + + // General mapping + // Store it in case we don't find the exact address mapping + if (addrmap->from.sin_port == 0) + found = addrmap; + } + + addrmap = addrmap->next; + } + + return found; +} + + +/* +==================== +Sv_ResolveAddrmap + +Resolve an addrmap structure and check the parameters validity +==================== +*/ +static qboolean Sv_ResolveAddrmap (addrmap_t* addrmap) +{ + // Resolve the addresses + if (!Sv_ResolveAddr (addrmap->from_string, &addrmap->from) || + !Sv_ResolveAddr (addrmap->to_string, &addrmap->to)) + return qfalse; + + // 0.0.0.0 addresses are forbidden + if (addrmap->from.sin_addr.s_addr == 0 || + addrmap->to.sin_addr.s_addr == 0) + { + MsgPrint (MSG_ERROR, + "ERROR: Mapping from or to 0.0.0.0 is forbidden\n"); + return qfalse; + } + + // Do NOT allow mapping to loopback addresses + if ((ntohl (addrmap->to.sin_addr.s_addr) >> 24) == 127) + { + MsgPrint (MSG_ERROR, + "ERROR: Mapping to a loopback address is forbidden\n"); + return qfalse; + } + + return qtrue; +} + + +/* +==================== +Sv_IsActive + +Return qtrue if a server is active. +Test if the server has timed out and remove it if it's the case. +==================== +*/ +static qboolean Sv_IsActive (server_t* server) +{ + + // If the entry isn't even used + if (! server->active) + return qfalse; + + // If the server has timed out + if (server->timeout < crt_time) + { + unsigned int hash; + server_t **prev, *sv; + + hash = Sv_AddressHash (&server->address); + prev = &hash_table[hash]; + sv = hash_table[hash]; + + while (sv != server) + { + prev = &sv->next; + sv = sv->next; + } + + Sv_RemoveAndGetNextPtr (sv, prev); + return qfalse; + } + + return qtrue; +} + + +// ---------- Public functions (servers) ---------- // + +/* +==================== +Sv_SetHashSize + +Set a new hash size value +==================== +*/ +qboolean Sv_SetHashSize (unsigned int size) +{ + // Too late? + if (hash_table != NULL) + return qfalse; + + // Too big? + if (size > MAX_HASH_SIZE) + return qfalse; + + hash_table_size = 1 << size; + return qtrue; +} + + +/* +==================== +Sv_SetMaxNbServers + +Set a new hash size value +==================== +*/ +qboolean Sv_SetMaxNbServers (unsigned int nb) +{ + // Too late? + if (servers != NULL) + return qfalse; + + max_nb_servers = nb; + return qtrue; +} + + +/* +==================== +Sv_Init + +Initialize the server list and hash table +==================== +*/ +qboolean Sv_Init (void) +{ + size_t array_size; + + // Allocate "servers" and clean it + array_size = max_nb_servers * sizeof (servers[0]); + servers = malloc (array_size); + if (!servers) + { + MsgPrint (MSG_ERROR, + "ERROR: can't allocate the servers array (%s)\n", + strerror (errno)); + return qfalse; + } + last_alloc = max_nb_servers - 1; + memset (servers, 0, array_size); + MsgPrint (MSG_NORMAL, "%u server records allocated\n", max_nb_servers); + + // Allocate "hash_table" and clean it + array_size = hash_table_size * sizeof (hash_table[0]); + hash_table = malloc (array_size); + if (!hash_table) + { + MsgPrint (MSG_ERROR, "ERROR: can't allocate the hash table (%s)\n", + strerror (errno)); + free (servers); + return qfalse; + } + memset (hash_table, 0, array_size); + MsgPrint (MSG_NORMAL, + "Hash table allocated (%u entries)\n", hash_table_size); + + return qtrue; +} + + +/* +==================== +Sv_GetByAddr + +Search for a particular server in the list; add it if necessary +==================== +*/ +server_t* Sv_GetByAddr (const struct sockaddr_in* address, qboolean add_it) +{ + server_t **prev, *sv; + unsigned int hash; + const addrmap_t* addrmap = Sv_GetAddrmap (address); + unsigned int startpt; + + // Allow servers on a loopback address ONLY if a mapping is defined for them + if ((ntohl (address->sin_addr.s_addr) >> 24) == 127 && addrmap == NULL) + { + MsgPrint (MSG_WARNING, + "WARNING: server %s isn't allowed (loopback address)\n", + peer_address); + return NULL; + } + + hash = Sv_AddressHash (address); + prev = &hash_table[hash]; + sv = hash_table[hash]; + + while (sv != NULL) + { + // We check the timeout values while browsing this list + if (sv->timeout < crt_time) + { + sv = Sv_RemoveAndGetNextPtr (sv, prev); + continue; + } + + // Found! + if (sv->address.sin_addr.s_addr == address->sin_addr.s_addr && + sv->address.sin_port == address->sin_port) + { + // Put it on top of the list (it's useful because heartbeats + // are almost always followed by infoResponses) + *prev = sv->next; + sv->next = hash_table[hash]; + hash_table[hash] = sv; + + return sv; + } + + prev = &sv->next; + sv = sv->next; + } + + if (! add_it) + return NULL; + + // Look for the first free entry in "servers" + startpt = last_alloc; + for (;;) + { + last_alloc = (last_alloc + 1) % max_nb_servers; + + // Free entry found? + if (!Sv_IsActive (&servers[last_alloc])) + break; + + // No more room + if (last_alloc == startpt) + return NULL; + } + sv = &servers[last_alloc]; + + // Initialize the structure + memset (sv, 0, sizeof (*sv)); + memcpy (&sv->address, address, sizeof (sv->address)); + sv->addrmap = addrmap; + + // Add it to the list it belongs to + sv->next = hash_table[hash]; + hash_table[hash] = sv; + nb_servers++; + + MsgPrint (MSG_NORMAL, + "New server added: %s; %u servers are currently registered\n", + peer_address, nb_servers); + MsgPrint (MSG_DEBUG, + " - index: %u\n" + " - hash: 0x%02X\n", + last_alloc, hash); + + return sv; +} + + +/* +==================== +Sv_GetFirst + +Get the first server in the list +==================== +*/ +server_t* Sv_GetFirst (void) +{ + crt_server = NULL; + prev_pointer = NULL; + crt_hash_ind = -1; + + return Sv_GetNext (); +} + + +/* +==================== +Sv_GetNext + +Get the next server in the list +==================== +*/ +server_t* Sv_GetNext (void) +{ + for (;;) + { + // If there is a current server, follow the link + if (crt_server != NULL) + { + prev_pointer = &crt_server->next; + crt_server = crt_server->next; + } + + // If we don't have the next server yet + if (crt_server == NULL) + { + // Search the hash table for the next server + while (crt_hash_ind < (int)(hash_table_size - 1)) + { + crt_hash_ind++; + + if (hash_table[crt_hash_ind] != NULL) + { + crt_server = hash_table[crt_hash_ind]; + prev_pointer = &hash_table[crt_hash_ind]; + break; + } + } + } + + // Did we hit the end of the list? + if (crt_server == NULL) + return NULL; + + // If the new current server has timed out, remove it + if (crt_server->timeout < crt_time) + crt_server = Sv_RemoveAndGetNextPtr (crt_server, prev_pointer); + else + return crt_server; + } +} + + +// ---------- Public functions (address mappings) ---------- // + +/* +==================== +Sv_AddAddressMapping + +Add an unresolved address mapping to the list +mapping must be of the form "addr1:port1=addr2:port2", ":portX" are optional +==================== +*/ +qboolean Sv_AddAddressMapping (const char* mapping) +{ + char *map_string, *to_ip; + addrmap_t* addrmap; + + // Get a working copy of the mapping string + map_string = strdup (mapping); + if (map_string == NULL) + { + MsgPrint (MSG_ERROR, + "ERROR: can't allocate address mapping string\n"); + return qfalse; + } + + // Find the '=' + to_ip = strchr (map_string, '='); + if (to_ip == NULL) + { + MsgPrint (MSG_ERROR, + "ERROR: invalid syntax in address mapping string\n"); + free (map_string); + return qfalse; + } + *to_ip++ = '\0'; + + // Allocate the structure + addrmap = malloc (sizeof (*addrmap)); + if (addrmap == NULL) + { + MsgPrint (MSG_ERROR, + "ERROR: can't allocate address mapping structure\n"); + free (map_string); + return qfalse; + } + memset (addrmap, 0, sizeof (*addrmap)); + addrmap->from_string = strdup (map_string); + addrmap->to_string = strdup (to_ip); + if (addrmap->from_string == NULL || addrmap->to_string == NULL) + { + MsgPrint (MSG_ERROR, + "ERROR: can't allocate address mapping strings\n"); + free (addrmap->to_string); + free (addrmap->from_string); + free (map_string); + return qfalse; + } + + // Add it on top of "addrmaps" + addrmap->next = addrmaps; + addrmaps = addrmap; + + return qtrue; +} + + +/* +==================== +Sv_ResolveAddressMappings + +Resolve the address mapping list +==================== +*/ +qboolean Sv_ResolveAddressMappings (void) +{ + addrmap_t* unresolved = addrmaps; + addrmap_t* addrmap; + qboolean succeeded = qtrue; + + addrmaps = NULL; + + while (unresolved != NULL) + { + // Remove it from the unresolved list + addrmap = unresolved; + unresolved = unresolved->next; + + // Continue the resolution, even if there's an error + if (!Sv_ResolveAddrmap (addrmap)) + { + free (addrmap->from_string); + free (addrmap->to_string); + free (addrmap); + succeeded = qfalse; + } + else + Sv_InsertAddrmapIntoList (addrmap); + } + + return succeeded; +} diff --git a/src/master/servers.h b/src/master/servers.h new file mode 100644 index 0000000..5864c03 --- /dev/null +++ b/src/master/servers.h @@ -0,0 +1,106 @@ +/* + servers.h + + Server list and address mapping management for dpmaster + + Copyright (C) 2004-2005 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#ifndef _SERVERS_H_ +#define _SERVERS_H_ + + +// ---------- Constants ---------- // + +// Maximum number of servers in all lists by default +#define DEFAULT_MAX_NB_SERVERS 1024 + +// Address hash size in bits (between 0 and MAX_HASH_SIZE) +#define DEFAULT_HASH_SIZE 6 +#define MAX_HASH_SIZE 8 + +// Number of characters in a challenge, including the '\0' +#define CHALLENGE_MIN_LENGTH 9 +#define CHALLENGE_MAX_LENGTH 12 + +// Minimum number of seconds between gamestat messages per server +#define MIN_GAMESTAT_DELAY 120 + +// ---------- Types ---------- // + +// Address mapping +typedef struct addrmap_s +{ + struct addrmap_s* next; + struct sockaddr_in from; + struct sockaddr_in to; + char* from_string; + char* to_string; +} addrmap_t; + +// Server properties +typedef struct server_s +{ + struct server_s* next; + struct sockaddr_in address; + unsigned int protocol; + char challenge [CHALLENGE_MAX_LENGTH]; + unsigned short nbclients; + unsigned short maxclients; + time_t timeout; + time_t challenge_timeout; + const struct addrmap_s* addrmap; + qboolean active; + time_t lastGameStat; +} server_t; + + +// ---------- Public functions (servers) ---------- // + +// Will simply return "false" if called after Sv_Init +qboolean Sv_SetHashSize (unsigned int size); +qboolean Sv_SetMaxNbServers (unsigned int nb); + +// Initialize the server list and hash table +qboolean Sv_Init (void); + +// Search for a particular server in the list; add it if necessary +// NOTE: doesn't change the current position for "Sv_GetNext" +server_t* Sv_GetByAddr (const struct sockaddr_in* address, qboolean add_it); + +// Get the first server in the list +server_t* Sv_GetFirst (void); + +// Get the next server in the list +server_t* Sv_GetNext (void); + + +// ---------- Public functions (address mappings) ---------- // + +// NOTE: this is a 2-step process because resolving address mappings directly +// during the parsing of the command line would cause several problems + +// Add an unresolved address mapping to the list +// mapping must be of the form "addr1:port1=addr2:port2", ":portX" are optional +qboolean Sv_AddAddressMapping (const char* mapping); + +// Resolve the address mapping list +qboolean Sv_ResolveAddressMappings (void); + + +#endif // #ifndef _SERVERS_H_ diff --git a/src/master/stats.c b/src/master/stats.c new file mode 100644 index 0000000..b4ea5f8 --- /dev/null +++ b/src/master/stats.c @@ -0,0 +1,136 @@ +/* +stats.c + +Statistics for tremmaster + +Copyright (C) 2009 Darklegion Development + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#ifndef _WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +#define MAX_DATA_SIZE 1024 +#define CS_FILENAME "clientStats.tdb" + +/* +==================== +RecordClientStat +==================== +*/ +void RecordClientStat( const char *address, const char *version, const char *renderer ) +{ + TDB_CONTEXT *tctx = NULL; + TDB_DATA key, data; + char ipText[ 22 ]; + char dataText[ MAX_DATA_SIZE ] = { 0 }; + char *p; + int i; + + tctx = tdb_open( CS_FILENAME, 0, 0, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR ); + + if( !tctx ) + { + MsgPrint( MSG_DEBUG, "Couldn't open %s\n", CS_FILENAME ); + return; + } + + strncpy( ipText, address, 22 ); + if( ( p = strrchr( ipText, ':' ) ) ) // Remove port + *p = '\0'; + + key.dptr = ipText; + key.dsize = strlen( ipText ); + + strncat( dataText, "\"", MAX_DATA_SIZE ); + strncat( dataText, version, MAX_DATA_SIZE ); + + // Remove last three tokens (the date) + for( i = 0; i < 3; i++ ) + { + if( ( p = strrchr( dataText, ' ' ) ) ) + *p = '\0'; + } + strncat( dataText, "\"", MAX_DATA_SIZE ); + + strncat( dataText, " \"", MAX_DATA_SIZE ); + strncat( dataText, renderer, MAX_DATA_SIZE ); + strncat( dataText, "\"", MAX_DATA_SIZE ); + + data.dptr = dataText; + data.dsize = strlen( dataText ); + + if( tdb_store( tctx, key, data, 0 ) < 0 ) + MsgPrint( MSG_DEBUG, "tdb_store failed\n" ); + + tdb_close( tctx ); + MsgPrint( MSG_DEBUG, "Recorded client stat for %s\n", address ); +} + +#define GS_FILENAME "gameStats.tdb" + +/* +==================== +RecordGameStat +==================== +*/ +void RecordGameStat( const char *address, const char *dataText ) +{ + TDB_CONTEXT *tctx = NULL; + TDB_DATA key, data; + char keyText[ MAX_DATA_SIZE ] = { 0 }; + char *p; + time_t tm = time( NULL ); + + tctx = tdb_open( GS_FILENAME, 0, 0, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR ); + + if( !tctx ) + { + MsgPrint( MSG_DEBUG, "Couldn't open %s\n", GS_FILENAME ); + return; + } + + strncpy( keyText, address, 22 ); + if( ( p = strrchr( keyText, ':' ) ) ) // Remove port + *p = '\0'; + + strncat( keyText, " ", MAX_DATA_SIZE ); + strncat( keyText, asctime( gmtime( &tm ) ), MAX_DATA_SIZE ); + + key.dptr = keyText; + key.dsize = strlen( keyText ); + + data.dptr = (char *)dataText; + data.dsize = strlen( dataText ); + + if( tdb_store( tctx, key, data, 0 ) < 0 ) + MsgPrint( MSG_DEBUG, "tdb_store failed\n" ); + + tdb_close( tctx ); + MsgPrint( MSG_NORMAL, "Recorded game stat from %s\n", address ); +} + +#endif -- cgit