Logo Search packages:      
Sourcecode: qstat version File versions  Download package

ut2004.c

/*
 * qstat 2.6
 * by Steve Jankowski
 *
 * UT2004 master query functions
 * Copyright 2004 Ludwig Nussel
 *
 * Licensed under the Artistic License, see LICENSE.txt for license terms
 *
 * This code is inspired by ideas from 'Nurulwai'
 *
 */

#include <sys/types.h>
#ifndef _WIN32
#include <sys/socket.h>
#endif
#include <stdlib.h>
#include <stdio.h>

#include "debug.h"
#include "qstat.h"
#include "md5.h"

/** \brief convert bytes into hex string
 *
 * \param in data to convert
 * \param len length of data
 * \param out location to store string to. Must be 2*len
 */
static void bin2hex(const char* in, size_t len, char* out);

#define CD_KEY_LENGTH     23

// arbitrary
#define MAX_LISTING_RECORD_LEN 0x04FF

#define RESPONSE_OFFSET_CDKEY     4
#define RESPONSE_OFFSET_CHALLENGE 38

static const char challenge_response[] = {
    0x68, 0x00, 0x00, 0x00,  '!', 0xCD, 0xCD, 0xCD, 
    /* length             |   !   MD5SUM, CD is placeholder */
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0x00,
                                         '!', 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 
    0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0x00, 

    0x0c,  'U',  'T',  '2',  'K',  '4',  'C',  'L',
    /*^^ 12 byte string  */
     'I',  'E',  'N',  'T', 0x00, 0xfb, 0x0c, 0x00, 
    /*                          | unknown        */
    0x00, 0x06, 0x04,  'i',  'n',  't', 0x00, 0x00, 
    /*        |   ^^ 4 byte string          | ?  */    
    0x00, 0x00, 0x00, 0xee, 0xee, 0x00, 0x00, 0x11, 
    /* unknown                                   */
    0x00, 0x00, 0x00, 0x01 };

static const char approved[] = {
    0x0e, 0x00, 0x00, 0x00, 0x09,  'A',  'P',  'P',
     'R',  'O',  'V',  'E',  'D', 0x00, 0x03, 0x00, 
    0x00, 0x00 };

static const char approved_response[] = {
    0x22, 0x00, 0x00, 0x00,  '!',  '0',  '0',  '0', 
     '0',  '0',  '0',  '0',  '0',  '0',  '0',  '0', 
     '0',  '0',  '0',  '0',  '0',  '0',  '0',  '0', 
     '0',  '0',  '0',  '0',  '0',  '0',  '0',  '0', 
     '0',  '0',  '0',  '0',  '0', 0x00 };

static const char verified[] = {
    0x0a, 0x00, 0x00, 0x00, 0x09,  'V',  'E',  'R',
     'I',  'F',  'I',  'E',  'D', 0x00 };

#if 0
struct server_listing_record_head {
    unsigned len;
    unsigned ip;
    short port;
    short queryport;
    char name[];
    // char map[]
};

struct server_listing_record_foot {
    unsigned char marker1[3];
    unsigned char unknown1;
    unsigned char maxplayers;
    unsigned char unknown2[4];
    unsigned char marker2[3];
};
#endif

static char cdkey[CD_KEY_LENGTH+1] = "";

enum ut2004_state {
    STATE_CHALLENGE = 0x00,
    STATE_APPROVED  = 0x01,
    STATE_VERIFIED  = 0x02,
    STATE_LISTING   = 0x03,
};

void send_ut2004master_request_packet(struct qserver *server)
{
    if(server->n_packets)
    {
      cleanup_qserver(server, 1);
      return;
    }

    if(!*cdkey)
    {
      char* param = get_param_value( server, "cdkey", NULL);
      if(!param)
      {
          debug(0, "Error: missing cdkey parameter");
          server->server_name = SYSERROR;
          cleanup_qserver(server, 1);
          return;
      }

      if(*param == '/')
      {
          FILE* fp = fopen(param, "r");
          if(!fp || fread(cdkey, 1, CD_KEY_LENGTH, fp) != CD_KEY_LENGTH)
          {
            debug(0, "Error: can't key from %s", param);
            server->server_name = SYSERROR;
            cleanup_qserver(server, 1);
            if(fp) fclose(fp);
            return;
          }
          fclose(fp);
      }
      else if(strchr(param, '-') && strlen(param) == CD_KEY_LENGTH)
      {
          memcpy(cdkey, param, CD_KEY_LENGTH);
      }
      else if(   *param == '$'
            && (param = getenv(param+1)) // replaces param!
            && strlen(param) == CD_KEY_LENGTH)
      {
          memcpy(cdkey, param, CD_KEY_LENGTH);
      }
      else
      {
          debug(0, "Error: invalid cdkey parameter");
          server->server_name = SYSERROR;
          cleanup_qserver(server, 1);
          return;
      }
    }

    qserver_send(server, NULL, 0);

#if 0
    // XXX since we do not send but rather expect a reply directly after
    // connect it's pointless to retry doing nothing
    debug(0, "retry1: %d", server->retry1);
    server->retry1 = 0;
#endif

    server->master_query_tag[0] = STATE_CHALLENGE;
}

static void ut2004_server_done(struct qserver* server)
{
    if(server->saved_data.next)
    {
      debug(0, "%d bytes of unprocessed data left. Premature EOF!?",
            server->saved_data.next->datalen);
      free(server->saved_data.next->data);
      free(server->saved_data.next);
      server->saved_data.next = NULL;
    }
}

// we use n_servers to store number of used bytes in master_pkt so
// it needs to be divided by 6 when finished
static void ut2004_parse_record(struct qserver* server, char* pkt)
{
    char* dest;

#if 0
    unsigned ip;
    unsigned short port;

    memcpy(&ip, pkt+4, 4);
    port = swap_short_from_little(pkt+4+4);

    debug(2, "got %d.%d.%d.%d:%hu", ip&0xff, (ip>>8)&0xff, (ip>>16)&0xff, (ip>>24)&0xff, port);
#endif

    if(server->n_servers+6 > server->master_pkt_len)
    {
      if(!server->master_pkt_len)
          server->master_pkt_len = 180;
      else
          server->master_pkt_len *= 2;
      server->master_pkt = (char*)realloc(server->master_pkt, server->master_pkt_len);
    }

    dest = server->master_pkt + server->n_servers;

    memcpy(dest, pkt+4, 4 );
    dest[4] = pkt[9];
    dest[5] = pkt[8];
    server->n_servers += 6;
}

static char* put_bytes(char* buf, const char* bytes, size_t len, size_t* left)
{
    if(!buf || len > *left)
      return NULL;

    memcpy(buf, bytes, len);
    *left -= len;

    return buf+len;
}

static char* put_string(char* buf, const char* string, size_t* left)
{
    size_t len = strlen(string)+1;
    char l;

    if(!buf || len > 0xFF || *left < len+1)
      return NULL;

    l = len;

    buf = put_bytes(buf, &l, 1, left);
    return put_bytes(buf, string, len, left);
}

/** \brief assemble the server filter and send the master query
  
  the query consists of four bytes length (excluding the four length bytes), a
  null byte and then the number of item pairs that follow.

  Each pair consists of two ut2 strings (length+null terminated string)
  followed by a byte which is either zero or 0x04 which means negate the query
  (e.g. not zero curplayers means not empty).
 */
static int ut2004_send_query(struct qserver* server)
{
    char buf[4096] = {0};
    size_t left = sizeof(buf);
    char *b = buf;
    char *param, *r, *sep= "";
    unsigned flen = 0;
    unsigned char items = 0;

    // header is done later
    b += 6;
    left -= 6;

    param = get_param_value( server, "gametype", NULL);
    if(param)
    {
      ++items;
      b = put_string(b, "gametype", &left);
      b = put_string(b, param, &left);
      b = put_bytes(b, "", 1, &left);
    }

    param = get_param_value( server, "status", NULL);
    r = param;
    while ( param && sep )
    {
      sep= strchr( r, ':');
      if ( sep )
          flen= sep-r;
      else
          flen= strlen(r);

      if (   strncmp( r, "standard", flen) == 0
          || strncmp( r, "nostandard", flen) == 0)
      {
          ++items;
          b = put_string(b, "standard", &left);
          if(*r == 'n')
            b = put_string(b, "false", &left);
          else
            b = put_string(b, "true", &left);
          b = put_bytes(b, "", 1, &left);
      }
      else if (  strncmp( r, "password", flen) == 0
            || strncmp( r, "nopassword", flen) == 0)
      {
          ++items;
          b = put_string(b, "password", &left);
          if(*r == 'n')
            b = put_string(b, "false", &left);
          else
            b = put_string(b, "true", &left);
          b = put_bytes(b, "", 1, &left);
      }
      else if ( strncmp( r, "notempty", flen) == 0)
      {
          ++items;
          b = put_string(b, "currentplayers", &left);
          b = put_string(b, "0", &left);
          b = put_bytes(b, "\x04", 1, &left);
      }
      else if ( strncmp( r, "notfull", flen) == 0)
      {
          ++items;
          b = put_string(b, "freespace", &left);
          b = put_string(b, "0", &left);
          b = put_bytes(b, "\x04", 1, &left);
      }
      else if ( strncmp( r, "nobots", flen) == 0)
      {
          ++items;
          b = put_string(b, "nobots", &left);
          b = put_string(b, "true", &left);
          b = put_bytes(b, "", 1, &left);
      }
      else if (  strncmp( r, "stats", flen) == 0
            || strncmp( r, "nostats", flen) == 0)
      {
          ++items;
          b = put_string(b, "stats", &left);
          if(*r == 'n')
            b = put_string(b, "false", &left);
          else
            b = put_string(b, "true", &left);
          b = put_bytes(b, "", 1, &left);
      }
      else if (  strncmp( r, "weaponstay", flen) == 0
            || strncmp( r, "noweaponstay", flen) == 0)
      {
          ++items;
          b = put_string(b, "weaponstay", &left);
          if(*r == 'n')
            b = put_string(b, "false", &left);
          else
            b = put_string(b, "true", &left);
          b = put_bytes(b, "", 1, &left);
      }
      else if (  strncmp( r, "transloc", flen) == 0
            || strncmp( r, "notransloc", flen) == 0)
      {
          ++items;
          b = put_string(b, "transloc", &left);
          if(*r == 'n')
            b = put_string(b, "false", &left);
          else
            b = put_string(b, "true", &left);
          b = put_bytes(b, "", 1, &left);
      }
      r= sep+1;
    }

    param = get_param_value( server, "mutator", NULL);
    r = param;
    sep = "";
    while ( param && sep )
    {
      char neg = '\0';
      unsigned char l;
      sep= strchr( r, ':');
      if ( sep )
          flen= sep-r;
      else
          flen= strlen(r);
      
      if(*r == '-')
      {
          neg = '\x04';
          ++r;
          --flen;
      }

      if(!flen)
          continue;

      b = put_string(b, "mutator", &left);
      l = flen+1;
      b = put_bytes(b, &l, 1, &left);
      b = put_bytes(b, r, flen, &left);
      b = put_bytes(b, "", 1, &left);
      b = put_bytes(b, &neg, 1, &left);
      ++items;

      r= sep+1;
    }

    if(!b)
    {
      debug(0, "Error: query buffer too small. Please file a bug report!");
      return 0;
    }

    put_long_little(b-buf-4, buf);
    buf[5] = items;

    return (qserver_send(server, buf, sizeof(buf)-left) > 0);
}

void deal_with_ut2004master_packet(struct qserver *server, char *rawpkt, int pktlen)
{
    unsigned char* state = &server->master_query_tag[0];

    md5_state_t md5;

    if(!pktlen)
    {
      ut2004_server_done(server);
      goto cleanup_out;
    }

    server->ping_total+= time_delta( &packet_recv_time, &server->packet_time1);

    switch(*state)
    {
      case STATE_CHALLENGE:
          // ensure at least one byte challenge, fit into buffer,
          // match challenge, null terminated
          if(    pktlen < 4 +1 +1 +1
            || pktlen > 4 +1 +8 +1
            || rawpkt[pktlen-1] != '\0')
          {
            malformed_packet(server, "invalid challenge" );
            goto cleanup_out;
          }
          else
          {
            char response[sizeof(challenge_response)];
            char* challenge = rawpkt+5;
            char sum[16];

            memcpy(response, challenge_response, sizeof(challenge_response));

            debug(2, "challenge: %s", challenge);

            md5_init(&md5);
            md5_append(&md5, cdkey, CD_KEY_LENGTH);
            md5_finish(&md5, sum);
            bin2hex(sum, 16, response+RESPONSE_OFFSET_CDKEY);

            md5_init(&md5);
            md5_append(&md5, cdkey, CD_KEY_LENGTH);
            md5_append(&md5, challenge, strlen(challenge));
            md5_finish(&md5, sum);
            bin2hex(sum, 16, response+RESPONSE_OFFSET_CHALLENGE);

            qserver_send(server, response, sizeof(response));

            server->server_name = MASTER;

            *state = STATE_APPROVED;
          }
          break;

      case STATE_APPROVED:

          if(pktlen != sizeof(approved)
            || 0 != memcmp(rawpkt, approved, pktlen))
          {
            malformed_packet(server, "CD key not approved" );
            goto cleanup_out;
          }

          debug(2, "got approval, sending verify");

          qserver_send(server, approved_response, sizeof(approved_response));
          *state = STATE_VERIFIED;

          break;

      case STATE_VERIFIED:

          if(pktlen != sizeof(verified)
            || 0 != memcmp(rawpkt, verified, pktlen))
          {
            malformed_packet(server, "CD key not verified" );
            goto cleanup_out;
          }

          if(!ut2004_send_query(server))
            goto cleanup_out;

          *state = STATE_LISTING;

          break;
      case STATE_LISTING:
          // first packet. contains number of servers to expect
          if(!server->saved_data.pkt_id)
          {
            /*
            server->saved_data.data = malloc(pktlen);
            memcpy(server->saved_data.data, rawpkt, pktlen);
            server->saved_data.datalen = pktlen;
            */
            server->saved_data.pkt_id = 1;

            if(pktlen == 9)
            {
                unsigned num = swap_long_from_little(rawpkt+4);
                debug(2, "expect %u servers", num);
#if 1
                if(num < 10000)
                {
                  server->master_pkt_len = num*6;
                  server->master_pkt = (char*)realloc(server->master_pkt, server->master_pkt_len);
                }
#endif
            }
          }
          else if(pktlen < 4)
          {
            malformed_packet(server, "packet too short");
            goto cleanup_out;
          }
          else
          {
            char* p = rawpkt;
            unsigned recordlen = 0;

            if(server->saved_data.next)
            {
                unsigned need = 0;
                SavedData* data = server->saved_data.next;
                // nasty, four bytes of record length are split up. since
                // we alloc'ed at least four bytes we just copy the 4-x
                // bytes to data->data
                if(data->datalen < 4)
                {
                  need = 4 - data->datalen;
                  debug(2, "need %d bytes more for recordlen", need);
                  if( need > pktlen)
                  {
                      // XXX ok, im lazy now. Stupid server can't even
                      // send four bytes in a row
                      malformed_packet(server, "chunk too small");
                      goto cleanup_out;
                  }
                  memcpy(data->data+data->datalen, p, need);
                  p += need;
                  data->datalen = 4;
                }

                recordlen = swap_long_from_little(data->data);

                if(!recordlen || recordlen > MAX_LISTING_RECORD_LEN)
                {
                  malformed_packet(server,
                        "record lengthx %x out of range, position %d", recordlen, p-rawpkt);
                  goto cleanup_out;
                }

                need = 4+recordlen - data->datalen;

                debug(2, "recordlen: %d, saved: %d, pkglen: %d, needed: %d", recordlen, data->datalen, pktlen, need);

                if( need <= pktlen)
                {
                  data->data = realloc(data->data, 4+recordlen);
                  memcpy(data->data + data->datalen, p, need);
                  ut2004_parse_record(server, data->data);
                  p += need;

                  free(data->data);
                  free(data);
                  server->saved_data.next = NULL;
                }
            }

            while(!server->saved_data.next && p-rawpkt+4 < pktlen)
            {
                recordlen = swap_long_from_little(p);

                // record too large
                if(!recordlen || recordlen > MAX_LISTING_RECORD_LEN)
                {
                  malformed_packet(server,
                        "record length %x out of range, position %d", recordlen, p-rawpkt);
                  goto cleanup_out;
                }
                // recordlen itself is four bytes
                recordlen += 4;

                // record fully inside packet
                if(p-rawpkt+recordlen <= pktlen)
                {
                  ut2004_parse_record(server, p);
                  p += recordlen;
                }
                else
                  break;
            }

            // record continues in next packet. save it.
            if(p-rawpkt < pktlen)
            {
                SavedData* data = server->saved_data.next;
                unsigned tosave = pktlen - (p-rawpkt);
                if(!data)
                {
                  data = malloc(sizeof(SavedData));
                  data->data = malloc(tosave<4?4:tosave); // alloc at least four bytes
                  data->datalen = tosave;
                  memcpy(data->data, p, data->datalen);
                  data->next = NULL;
                  server->saved_data.next = data;
                  
                  debug(1, "saved %d bytes", data->datalen );
                }
                else
                {
                  data->data = realloc(data->data, data->datalen + tosave );
                  memcpy(data->data+data->datalen, p, tosave);
                  data->datalen += tosave;
                  
                  debug(1, "saved %d bytes (+)", data->datalen );
                }
            }
          }
          break;
    }

#if 0 // would cause extremely short timeout
    server->next_player_info= -1; // would clean up otherwise <- not true?
#endif


    debug(2, "%d servers total", server->n_servers/6);

#if 0 // harms now with new ping scheduling
    server->retry1= 0;
    cleanup_qserver( server, 0);
    bind_sockets();
#endif

    return;

cleanup_out:
    server->master_pkt_len = server->n_servers;
    server->n_servers /= 6;
    cleanup_qserver(server, 1);
    return;
}

static const char hexchar[] = "0123456789abcdef";

static void bin2hex(const char* in, size_t len, char* out)
{
    char* o = out+len*2;
    in += len;
    do
    {
      *o-- = hexchar[*--in&0x0F];
      *o-- = hexchar[(*in>>4)&0x0F];
    } while(o != out);
}

// vim: sw=4 ts=8 noet

Generated by  Doxygen 1.6.0   Back to index