dxx-rebirth/tracker/client/tracker_client.c

845 lines
24 KiB
C

/*
This is code needed to contact and interact with the tracker server. Contact
me at kip@thevertigo.com for comments. Yes, I thought about GGZ, but it wasn't
fully portable at the time of writing.
This file and the acompanying tracker.h file are free software;
you can redistribute them and/or modify them under the terms of the GNU
Library General Public License as published by the Free Software Foundation;
either version 2 of the License, or (at your option) any later version.
These files are distributed in the hope that they will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with these files; see the file COPYING.LIB. If not,
write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.
*/
// Includes...
#include <key.h>
#include <config.h>
#include <arpa/inet.h>
#include "dl_list.h"
#include "error.h"
#include <errno.h>
#include <multi.h>
#include <netdb.h>
#include "netdrv.h"
#include "newmenu.h"
#include <SDL/SDL.h>
#include <SDL/SDL_thread.h>
#include "text.h"
#include "tracker/tracker.h"
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <sys/unistd.h>
#include "u_mem.h"
// Maximum number of games to list...
#define TRACKER_MAX_GAMES 64
// Truth...
#ifndef TRUE
#define TRUE 1
#endif
// Falsity...
#ifndef FALSE
#define FALSE !TRUE
#endif
// Private functions...
// Tracker communication thread...
static int TrackerCommunicationThread(void *pThreadData);
// Receive a message from tracker, returning non-zero if ok...
static int TrackerReceive(
int Socket, char *pszBuffer, unsigned int const unSize);
// Remove the game if we have it locally tracked...
static void TrackerRemoveGame(char const *pszDescription);
// Transmit a message to tracker, returning non-zero if ok. If unSize is
// zero, assumes null terminated string in pszMessage...
static int TrackerSend(
int Socket, char const *pszMessage, unsigned int unSize);
// Convert ASCII address or hostname into a socket address...
static struct in_addr *TrackerStringToAddress(
char const *pszAddress, struct in_addr *pAddressBuffer);
// Toggle the error flag and error message. Needed for showing errors from
// secondary threads...
static void TrackerThreadSetError(char const *pszError);
// Callback to update menu GUI...
static void TrackerUpdateBrowseMenuCallback(
int nItems, newmenu_item *pMenuItems, int *pnLastKey, int nCurrentItem);
// Current status states...
typedef enum
{
Null, /* Nothing happening yet */
Initializing, /* Initializing stuff */
Connecting, /* Connecting to the tracker server */
Refreshing, /* Refreshing */
}TrackerState;
// TrackerNetGame structure...
typedef struct
{
// Name / description...
char szDescription[NETGAME_NAME_LEN + 1];
// Hostname / IP address...
char szAddress[128];
// Port...
unsigned short usPort;
}TrackerNetGame;
// Tracker data used to communicate with communication thread...
struct
{
// Mutex...
SDL_mutex *pMutex;
// Status...
TrackerState State;
// Game list...
dl_list *GameList;
// Toggle to tell tracker thread to gracefuly exit...
unsigned char AbortRequested;
// An error was detected...
unsigned char ErrorDetected;
char szError[256];
}TrackerData;
// Callback to update menu GUI...
static void TrackerUpdateBrowseMenuCallback(
int nItems, newmenu_item *pMenuItems, int *pnLastKey, int nCurrentItem)
{
// Variables...
TrackerNetGame *pCurrentGame = NULL;
unsigned int unCurrentMenuItem = 0;
// Lock the tracker mutex...
SDL_LockMutex(TrackerData.pMutex);
// An error was detected, display it...
if(TrackerData.ErrorDetected)
{
// Display the error message...
nm_messagebox(NULL, 1, TXT_OK, TrackerData.szError);
// Unlock the tracker mutex...
SDL_UnlockMutex(TrackerData.pMutex);
// Menu should disappear now...
*pnLastKey = KEY_ESC;
// Done...
return;
}
// Abort requested...
if(TrackerData.AbortRequested)
{
// Update the status...
nItems = 1;
pMenuItems[0].type = NM_TYPE_TEXT;
pMenuItems[0].text = "Aborting, please wait...";
// Unlock the tracker mutex...
SDL_UnlockMutex(TrackerData.pMutex);
// Done...
return;
}
// What state is the tracker in?
switch(TrackerData.State)
{
// Nothing...
default:
case Null:
{
// Update the status...
nItems = 1;
pMenuItems[0].type = NM_TYPE_TEXT;
pMenuItems[0].text = "";
// Done...
break;
}
// Initializing...
case Initializing:
{
// Update the status...
nItems = 1;
pMenuItems[0].type = NM_TYPE_TEXT;
pMenuItems[0].text = "Initializing, please wait...";
// Done...
break;
}
// Connecting...
case Connecting:
{
// Update the status...
nItems = 1;
pMenuItems[0].type = NM_TYPE_TEXT;
pMenuItems[0].text = "Connecting to tracker server...";
// Done...
break;
}
// Refreshing...
case Refreshing:
{
// Add the status item and separator...
nItems = 1;
pMenuItems[0].type = NM_TYPE_TEXT;
pMenuItems[0].text = "Refreshing...";
// We have games received, use them instead...
if(dl_size(TrackerData.GameList) > 0)
{
// We'll use that many menu items to list each tracked game...
nItems = dl_size(TrackerData.GameList);
// Populate GUI with game list...
// Seek to beginning of game list...
TrackerData.GameList->current = TrackerData.GameList->first;
// Start filling in menu items from beginning...
unCurrentMenuItem = 0;
// Add each game...
do
{
// Extract entry...
pCurrentGame = (TrackerNetGame *) TrackerData.GameList->current->data;
// Fill in menu item...
pMenuItems[unCurrentMenuItem].type = NM_TYPE_MENU;
pMenuItems[unCurrentMenuItem].text = pCurrentGame->szDescription;
// Move to next menu item slot...
unCurrentMenuItem++;
}
while(dl_forward(TrackerData.GameList));
}
// Done...
break;
}
}
// Unlock the tracker mutex...
SDL_UnlockMutex(TrackerData.pMutex);
}
// Tracker communication thread...
static int TrackerCommunicationThread(void *pThreadData)
{
// Variables...
int Socket = 0;
struct in_addr ServerAddress;
struct sockaddr_in ServerSocketAddress;
int nStatus = 0;
char szBuffer[1024] = {0};
TrackerNetGame *pCurrentGame = NULL;
char szDescription[NETGAME_NAME_LEN + 1] = {0};
// Initializing...
// Update GUI...
SDL_LockMutex(TrackerData.pMutex);
TrackerData.State = Initializing;
SDL_UnlockMutex(TrackerData.pMutex);
// Allocate socket...
Socket = socket(PF_INET, SOCK_STREAM, 0);
// Failed...
if(Socket < 0)
{
// Set error...
TrackerThreadSetError("Unable to allocate socket.");
// Abort...
return 0;
}
// Switch to non-blocking...
if(fcntl(Socket, F_SETFL, O_NONBLOCK) != 0)
{
// Cleanup...
close(Socket);
// Set error and abort...
TrackerThreadSetError("Unable to switch to non-blocking mode.");
// Abort...
return 0;
}
// Connecting...
// Update GUI...
SDL_LockMutex(TrackerData.pMutex);
TrackerData.State = Connecting;
SDL_UnlockMutex(TrackerData.pMutex);
// Clear out server address...
memset(&ServerAddress, 0, sizeof(struct in_addr));
// Resolve address and check for error...
if(!TrackerStringToAddress(GameCfg.TrackerServer, &ServerAddress))
{
// Cleanup...
close(Socket);
// Set error...
TrackerThreadSetError("Unable to contact game tracker.");
// Abort...
return 0;
}
// Initialize socket address...
memset(&ServerSocketAddress, 0, sizeof(struct sockaddr_in));
ServerSocketAddress.sin_family = AF_INET;
ServerSocketAddress.sin_port = htons(TRACKER_PORT);
ServerSocketAddress.sin_addr.s_addr = ServerAddress.s_addr;
// Connect...
while(TRUE)
{
// Try to connect...
nStatus = connect(Socket, (struct sockaddr *) &ServerSocketAddress,
sizeof(ServerSocketAddress));
// Connect completed...
if(nStatus == 0)
break;
// Something else happened...
switch(errno)
{
// Connection already in progress...
case EINPROGRESS:
{
// User triggered an abort...
if(TrackerData.AbortRequested)
{
// Cleanup...
close(Socket);
// Abort...
return 0;
}
// Don't spin the clock...
SDL_Delay(50);
// Give it some more time...
continue;
}
// Connection refused...
case ECONNREFUSED:
{
// Cleanup...
close(Socket);
// Alert user...
TrackerThreadSetError("Tracker refused connection.");
// Done...
return 0;
}
// Some other unknown error...
default:
{
// Cleanup...
close(Socket);
// Alert user...
TrackerThreadSetError("Unknown tracker error.");
// Done...
return 0;
}
}
}
// Handshake...
if(!TrackerSend(Socket, "MATERIAL\n", 0) ||
!TrackerReceive(Socket, szBuffer, sizeof(szBuffer)) ||
strcmp(szBuffer, "DEFENDER\n") ||
!TrackerSend(Socket,
"USERAGENT " PROGRAM_NAME " " D1XMAJOR " " D1XMINOR "\n", 0) ||
!TrackerReceive(Socket, szBuffer, sizeof(szBuffer)))
{
// Cleanup...
close(Socket);
// Alert user...
TrackerThreadSetError("Tracker handshake failed.");
// Done...
return 0;
}
// Check if user agent was not accepted...
if(strncmp(szBuffer, "FAIL", strlen("FAIL")) == 0)
{
// Cleanup...
close(Socket);
// Alert user with server message, if any...
if(strlen(szBuffer) > strlen("FAIL\n"))
TrackerThreadSetError(&szBuffer[strlen("FAIL") + 1]);
// Otherwise, use default...
else
TrackerThreadSetError("Your client was not accepted.");
// Done...
return 0;
}
// Chomp user agent accepted and send ready...
if(strcmp(szBuffer, "OK\n") != 0)
{
// Cleanup...
close(Socket);
// Alert user...
TrackerThreadSetError("Tracker sent garbage.");
// Done...
return 0;
}
// Update GUI...
SDL_LockMutex(TrackerData.pMutex);
TrackerData.State = Refreshing;
SDL_UnlockMutex(TrackerData.pMutex);
// Keep refreshing until we can't anymore...
while(!TrackerData.AbortRequested)
{
// Receive a line and check for error...
if(!TrackerReceive(Socket, szBuffer, sizeof(szBuffer)))
{
// Alert user...
TrackerThreadSetError("Lost connection with tracker.");
// Abort...
break;
}
// Process notification...
// Server wants us to add a game to our list...
if(strncmp(szBuffer, "GAME_ADD ", strlen("GAME_ADD ")) == 0)
{
// Allocate a new game structure...
pCurrentGame = d_malloc(sizeof(TrackerNetGame));
// Clear it...
memset(pCurrentGame, '\x0', sizeof(TrackerNetGame));
// Parse it and check for error...
/* GAME_ADD <address>:<port> "<description>" */
if(sscanf(szBuffer, "GAME_ADD %s %hu \"%15[^\"]s",
pCurrentGame->szAddress,
&pCurrentGame->usPort,
pCurrentGame->szDescription) < 3)
{
// Alert user...
TrackerThreadSetError("Tracker list contained garbage.");
// Abort...
break;
}
// Store in game list...
SDL_LockMutex(TrackerData.pMutex);
dl_add(TrackerData.GameList, pCurrentGame);
SDL_UnlockMutex(TrackerData.pMutex);
}
// Server wants us to remove a game from our list...
else if(strncmp(szBuffer, "GAME_REM ", strlen("GAME_REM ")) == 0)
{
// Parse it and check for error...
/* GAME_REM "<description>" */
if(sscanf(szBuffer, "GAME_REM \"%[^\"]s", szDescription) != 1)
{
// Alert user...
TrackerThreadSetError("Tracker sent garbage.");
// Abort...
break;
}
// Remove the game if we have it locally tracked...
TrackerRemoveGame(szDescription);
}
// Server wants us to display an alert...
else if(strncmp(szBuffer, "ALERT ", strlen("ALERT ")) == 0)
{
puts("Alert...");
}
// Unknown...
else
{
// Alert user...
TrackerThreadSetError("Received unknown notification.");
// Abort...
break;
}
// Lock tracker data...
SDL_LockMutex(TrackerData.pMutex);
TrackerData.State = Refreshing;
SDL_UnlockMutex(TrackerData.pMutex);
}
// Cleanup...
close(Socket);
// Stubbed...
return 0;
}
// Show the browse netgames menu...
void TrackerBrowseMenu()
{
// Variables...
newmenu_item MenuItems[TRACKER_MAX_GAMES];
SDL_Thread *pCommunicationThread = NULL;
// This isn't available in release mode yet...
#ifdef NDEBUG
// Alert user...
nm_messagebox(NULL, 1, TXT_OK, "Sorry, but NetGame browsing is not\n"
"enabled in release mode yet.");
// Done...
return;
#endif
// Initialize tracker data...
memset(&TrackerData, '\x0', sizeof(TrackerData));
// Create mutex...
TrackerData.pMutex = SDL_CreateMutex();
// Failed...
if(!TrackerData.pMutex)
Error("Failed to create tracker mutex...");
// State...
TrackerData.State = Null;
// Game list...
TrackerData.GameList = dl_init();
// Abort flag...
TrackerData.AbortRequested = 0;
// Error stuff...
TrackerData.ErrorDetected = 0;
strcpy(TrackerData.szError, "");
// Launch the communication thread...
pCommunicationThread = SDL_CreateThread(TrackerCommunicationThread, NULL);
// Failed... (not sure if SDL has its own handler already, oh well)
if(!pCommunicationThread)
Error("Failed to spawn communication thread.");
// Initialize the stubbed menu...
memset(MenuItems, '\x0', sizeof(MenuItems));
MenuItems[0].type = NM_TYPE_TEXT;
MenuItems[0].text = " ";
// Display the GUI...
int const nMenuReturn = newmenu_do(
"UDP/IP NetGames", NULL, 1, MenuItems, TrackerUpdateBrowseMenuCallback);
// Signal to communication thread to terminate gracefully...
TrackerData.AbortRequested = 1;
// Wait for thread to die...
SDL_WaitThread(pCommunicationThread, NULL);
// Destroy the mutex...
SDL_DestroyMutex(TrackerData.pMutex);
TrackerData.pMutex = NULL;
// Cleanup the game list if allocated...
if(TrackerData.GameList)
{
// Free each node...
while(TrackerData.GameList->first)
{
// Free the node's data first...
d_free(TrackerData.GameList->first->data);
TrackerData.GameList->first->data = NULL;
// Now the node...
dl_remove(TrackerData.GameList, TrackerData.GameList->first);
}
// Free the list itself...
d_free(TrackerData.GameList);
TrackerData.GameList = NULL;
}
// User hit escape... (or something else unexpected)
if(nMenuReturn < 0)
return;
}
// Receive a message from tracker, returning non-zero if everything went ok...
static int TrackerReceive(
int Socket, char *pszBuffer, unsigned int const unSize)
{
// Variables...
unsigned int unReceived = 0;
int nStatus = 0;
// Not enough space to retrieve anything...
if(unSize < 1)
return FALSE;
// Clear receive buffer...
memset(pszBuffer, 0, unSize);
// Receive loop...
while(unReceived < unSize)
{
// Try to receive some data...
nStatus = recv(Socket, &pszBuffer[unReceived], 1, 0);
// An error occurred...
if(nStatus < 0)
{
// What happened?
switch(errno)
{
// We were asked to try receiving again...
case EAGAIN:
{
// User aborted...
if(TrackerData.AbortRequested)
return FALSE;
// Try again in a bit...
SDL_Delay(100);
continue;
}
// Some other error...
default:
return FALSE;
}
}
// Tracker closed connection on us...
else if(nStatus == 0)
return FALSE;
// Received a byte...
else
{
// This is the end of a server message...
if(pszBuffer[unReceived] == '\n')
return TRUE;
// Update counter...
unReceived += nStatus;
}
}
// Ran out of space...
return FALSE;
}
// Remove the game if we have it locally tracked...
static void TrackerRemoveGame(char const *pszDescription)
{
// Variables...
TrackerNetGame *pCurrentGame = NULL;
// Lock resources...
SDL_LockMutex(TrackerData.pMutex);
// List is empty, so nothing to remove...
if(dl_is_empty(TrackerData.GameList))
return;
// Seek to beginning of game list...
TrackerData.GameList->current = TrackerData.GameList->first;
// Search for the game...
do
{
// Extract entry...
pCurrentGame = (TrackerNetGame *) TrackerData.GameList->current->data;
// Match...
if(strcmp(pCurrentGame->szDescription, pszDescription) == 0)
{
// Remove game...
dl_remove(TrackerData.GameList, TrackerData.GameList->current);
// Unlock resources...
SDL_UnlockMutex(TrackerData.pMutex);
// Done...
return;
}
}
while(dl_forward(TrackerData.GameList));
// Unlock resources...
SDL_UnlockMutex(TrackerData.pMutex);
}
// Transmit a message to tracker, returning non-zero if ok. If unSize is zero,
// assumes null terminated string in pszMessage...
static int TrackerSend(
int Socket, char const *pszMessage, unsigned int unSize)
{
// Variables...
unsigned int unSent = 0;
unsigned int unRemaining = 0;
int nStatus = 0;
// Size not specified, so must be null terminated string...
if(!unSize)
unRemaining = unSize = strlen(pszMessage);
// Size already specified...
else
unRemaining = unSize;
// Keep trying to send data until none left to send...
while(unSent < unSize)
{
// Transmit...
nStatus = send(Socket, &pszMessage[unSent], unRemaining, 0);
// Something interesting happened...
if(nStatus < 0)
{
// What?
switch(errno)
{
// We were asked to try sending again...
case EAGAIN:
{
// User aborted...
if(TrackerData.AbortRequested)
return FALSE;
}
// Some other error...
default:
return FALSE;
}
}
// Update counters...
unSent += nStatus;
unRemaining -= nStatus;
}
// Done...
return TRUE;
}
// Convert ASCII address or hostname into a socket address...
static struct in_addr *TrackerStringToAddress(
char const *pszAddress, struct in_addr *pAddressBuffer)
{
// Variables...
struct hostent *pHostList = NULL;
// Clear address...
memset(pAddressBuffer, 0, sizeof(struct in_addr));
// Is this an IP address?
pAddressBuffer->s_addr = inet_addr(pszAddress);
// Yes, use the new in_addr format...
if(pAddressBuffer->s_addr != -1)
return pAddressBuffer;
// No, it's probably a host name. Try and resolve...
pHostList = gethostbyname(pszAddress);
// Lookup successfull...
if(pHostList != NULL)
{
// Store first address...
memcpy(pAddressBuffer, (struct in_addr *) *pHostList->h_addr_list,
sizeof(struct in_addr));
// Return to caller...
return pAddressBuffer;
}
// Unknown or DNS down...
return NULL;
}
// Toggle the error flag and error message. Needed for showing errors from
// secondary threads...
static void TrackerThreadSetError(char const *pszError)
{
// Lock resources...
SDL_LockMutex(TrackerData.pMutex);
// Toggle error and abort flag...
TrackerData.AbortRequested = TRUE;
TrackerData.ErrorDetected = TRUE;
// Store error message...
strcpy(TrackerData.szError, pszError);
// Unlock resources...
SDL_UnlockMutex(TrackerData.pMutex);
}