├── info ├── Makefile.am ├── UT2003.txt ├── a2s.txt └── GhostRecon.txt ├── template ├── Makefile.am ├── ghostrecon.lst ├── brocTp.html ├── unrealTp.html ├── brocTt.html ├── tribes2tt.html ├── unrealTt.html ├── ghostreconTt.html ├── ghostreconTp.html ├── ghostreconTh.html ├── tribes2tp.html ├── unrealTh.html ├── tribes2th.html ├── brocTs.html ├── unrealTs.html ├── tribes2ts.html ├── brocTh.html ├── README.txt └── ghostreconTs.html ├── CHANGES.txt ├── tests ├── etqw_1.4_test1 ├── etqw_1.4_test2 ├── etqws.pl └── xml.pl ├── .gitignore ├── version.h.tmpl ├── config.h ├── armyops.h ├── xform.h ├── tf.h ├── scripts ├── version.sh └── gen-version.cmd ├── ksp.h ├── tm.h ├── ut2004.h ├── gps.h ├── ts2.h ├── ts3.h ├── wic.h ├── crysis.h ├── gs2.h ├── mumble.h ├── farmsim.h ├── starmade.h ├── bfbc2.h ├── cube2.h ├── dirtybomb.h ├── terraria.h ├── fl.h ├── a2s.h ├── armyops.c ├── gs3.h ├── ottd.h ├── ventrilo.h ├── tee.h ├── ChangeLog ├── packet_manip.h ├── haze.h ├── COMPILE.md ├── .github └── workflows │ ├── build.yaml │ └── release.yaml ├── doom3.h ├── contrib.cfg ├── utils.h ├── autogen.sh ├── mumble.c ├── debug.h ├── Makefile.am ├── display_json.h ├── configure.ac ├── Makefile.noauto ├── bfbc2.c ├── md5.h ├── qserver.c ├── ts2.c ├── debug.c ├── README.md ├── utils.c ├── wic.c ├── terraria.c ├── cube2.c ├── farmsim.c ├── ksp.c ├── starmade.c ├── crysis.c ├── qserver.h ├── packet_manip.c ├── gs2.c ├── LICENSE.txt ├── tm.c ├── ottd.c ├── dirtybomb.c └── gps.c /info/Makefile.am: -------------------------------------------------------------------------------- 1 | EXTRA_DIST = $(wildcard *.txt) 2 | -------------------------------------------------------------------------------- /template/Makefile.am: -------------------------------------------------------------------------------- 1 | EXTRA_DIST = $(wildcard *.html *.txt) 2 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/qstat/HEAD/CHANGES.txt -------------------------------------------------------------------------------- /template/ghostrecon.lst: -------------------------------------------------------------------------------- 1 | grs 208.144.248.104 2 | grs,ignoreserverplayer=yes 208.144.248.105 3 | -------------------------------------------------------------------------------- /tests/etqw_1.4_test1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/qstat/HEAD/tests/etqw_1.4_test1 -------------------------------------------------------------------------------- /tests/etqw_1.4_test2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/qstat/HEAD/tests/etqw_1.4_test2 -------------------------------------------------------------------------------- /template/brocTp.html: -------------------------------------------------------------------------------- 1 | 2 |   3 | $PLAYERNAME 4 | $FRAGS 5 | $PLAYERPING 6 | -------------------------------------------------------------------------------- /template/unrealTp.html: -------------------------------------------------------------------------------- 1 | 2 |   3 | $PLAYERNAME 4 | $FRAGS 5 | $SKIN 6 | $TEAMNUM 7 | -------------------------------------------------------------------------------- /template/brocTt.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

$TOTALPLAYERS players on $TOTALUP servers. 4 |

Created with QStat $QSTATVERSION 5 |

6 | -------------------------------------------------------------------------------- /template/tribes2tt.html: -------------------------------------------------------------------------------- 1 | 2 |


3 |

$TOTALPLAYERS players on $TOTALUP servers. 4 |

Created with QStat $QSTATVERSION 5 |

6 | -------------------------------------------------------------------------------- /template/unrealTt.html: -------------------------------------------------------------------------------- 1 | 2 |


3 |

$TOTALPLAYERS players on $TOTALUP servers. 4 |

Created with QStat $QSTATVERSION 5 |

6 | -------------------------------------------------------------------------------- /template/ghostreconTt.html: -------------------------------------------------------------------------------- 1 | 2 |


3 |

$TOTALPLAYERS players on $TOTALUP servers. 4 |

Created with QStat $QSTATVERSION 5 |

6 | -------------------------------------------------------------------------------- /template/ghostreconTp.html: -------------------------------------------------------------------------------- 1 | 2 | $PLAYERNAME 3 | $TEAMNAME 4 | $(IF:DEATHS)Dead$(ENDIF)$(IFNOT:DEATHS)Alive$(ENDIF) 5 |   6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.in 2 | aclocal.m4 3 | autom4te.cache 4 | compile 5 | config.guess 6 | config.sub 7 | config.log 8 | config.status 9 | configure 10 | depcomp 11 | install-sh 12 | missing 13 | .deps 14 | Makefile 15 | gnuconfig.h 16 | stamp-h1 17 | *.o 18 | *.obj 19 | *.pdb 20 | qstat 21 | qstat.exe 22 | .compiler_flags 23 | .version 24 | version.h 25 | -------------------------------------------------------------------------------- /version.h.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Copyright 2021 Steven Hartland 6 | * 7 | * Licensed under the Artistic License, see LICENSE.txt for license terms 8 | */ 9 | #ifndef QSTAT_VERSION_H 10 | #define QSTAT_VERSION_H 11 | 12 | #ifndef QSTAT_VERSION 13 | #define QSTAT_VERSION "CHANGEME" 14 | #endif 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * config.h 3 | * by Steve Jankowski 4 | * steve@qstat.org 5 | * http://www.qstat.org 6 | * 7 | * Copyright 1996,1997,1998,1999 by Steve Jankowski 8 | */ 9 | 10 | #include "qstat.h" 11 | 12 | int qsc_load_default_config_files(); 13 | int qsc_load_config_file(char const *filename); 14 | server_type **qsc_get_config_server_types(int *n_config_types); 15 | -------------------------------------------------------------------------------- /armyops.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * 4 | * Armyops Protocol 5 | * Copyright 2005 Omnix 6 | * 7 | * Licensed under the Artistic License, see LICENSE.txt for license terms 8 | */ 9 | #ifndef QSTAT_ARMOPS_H 10 | #define QSTAT_ARMOPS_H 11 | 12 | int calculate_armyops_score(struct player *player); 13 | void json_display_armyops_player_info(struct qserver *server); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /template/ghostreconTh.html: -------------------------------------------------------------------------------- 1 | $HTML 2 | 3 | Ghost Recon Servers 4 | 5 | 6 | 7 | 8 | 9 |

Ghost Recon Servers

10 | Last update: $NOW 11 | 12 |
13 |

Example of Ghost Recon support in QStat.
14 |

15 |

16 | -------------------------------------------------------------------------------- /xform.h: -------------------------------------------------------------------------------- 1 | /* 2 | * xform Functions 3 | * Copyright 2013 Steven Hartland 4 | * 5 | * Licensed under the Artistic License, see LICENSE.txt for license terms 6 | */ 7 | #ifndef QSTAT_XFORM_H 8 | #define QSTAT_XFORM_H 9 | 10 | #include "qstat.h" 11 | 12 | void xform_buf_free(); 13 | int xform_printf(FILE *, const char *, ...); 14 | char *xform_name(char *, struct qserver *); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /tf.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * 4 | * Titafall protocol 5 | * Copyright 2015 Steven Hartland 6 | * 7 | * Licensed under the Artistic License, see LICENSE.txt for license terms 8 | */ 9 | #ifndef QSTAT_TF_H 10 | #define QSTAT_TF_H 11 | 12 | #include "qserver.h" 13 | 14 | query_status_t send_tf_request_packet(struct qserver *server); 15 | query_status_t deal_with_tf_packet(struct qserver *server, char *rawpkt, int pktlen); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$QSTAT_VERSION" = "" ]; then 4 | QSTAT_VERSION=`git describe --tags --always` 5 | fi 6 | 7 | # Detect if our git repo is in a dirty state (uncommitted changes) 8 | GIT_DIRTY_FILE=`git status --porcelain 2>/dev/null| grep -Fv 'gnuconfig.h.in~' | egrep "^(M| M|\?\?)" | wc -l | sed -e 's/^[[:space:]]*//'` 9 | if [ ${GIT_DIRTY_FILE} -ne 0 ]; then 10 | QSTAT_VERSION="$QSTAT_VERSION-modified" 11 | fi 12 | 13 | echo -n $QSTAT_VERSION 14 | -------------------------------------------------------------------------------- /ksp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * KSP query protocol 6 | * Copyright 2014 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_KSP_H 11 | #define QSTAT_KSP_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_ksp_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_ksp_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /tm.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * TrackMania protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_TM_H 11 | #define QSTAT_TM_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_tm_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_tm_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /ut2004.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * debug helper functions 6 | * Copyright 2004 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_UT2004_H 11 | #define QSTAT_UT2004_H 12 | 13 | #include "qstat.h" 14 | 15 | query_status_t send_ut2004master_request_packet(struct qserver *server); 16 | query_status_t deal_with_ut2004master_packet(struct qserver *server, char *rawpkt, int pktlen); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /gps.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Gamespy query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_GPS_H 11 | #define QSTAT_GPS_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_gps_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_gps_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /ts2.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Teamspeak 2 protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_TS2_H 11 | #define QSTAT_TS2_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_ts2_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_ts2_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /ts3.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Teamspeak 3 protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_TS3_H 11 | #define QSTAT_TS3_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_ts3_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_ts3_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /template/tribes2tp.html: -------------------------------------------------------------------------------- 1 | 2 |   3 | $HTMLPLAYERNAME 4 | $(IF:ISBOT)Bot$(ENDIF)$(IF:ISALIAS)Alias$(ENDIF)$(IF:ISTEAM) $ENDIF 5 | $(IF:TRIBETAG)$TRIBETAG$(ENDIF)$(IF:ISTEAM) $ENDIF 6 | $FRAGS 7 |   8 | $TEAMNAME$(IF:ISTEAM)TEAM$ENDIF 9 | -------------------------------------------------------------------------------- /wic.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * World in Conflict Protocol 6 | * Copyright 2007 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_WIC_H 11 | #define QSTAT_WIC_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_wic_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_wic_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /crysis.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Crysis protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_CRYSIS_H 11 | #define QSTAT_CRYSIS_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_crysis_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_crysis_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /gs2.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * New Gamespy v2 query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_GS2_H 11 | #define QSTAT_GS2_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_gs2_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_gs2_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /mumble.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Mumble protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_MUMBLE_H 11 | #define QSTAT_MUMBLE_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_mumble_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_mumble_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /farmsim.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Crysis protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_FARMSIM_H 11 | #define QSTAT_FARMSIM_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_farmsim_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_farmsim_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /starmade.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * StarMade protocol 6 | * Copyright 2013 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_STARMADE_H 11 | #define QSTAT_STARMADE_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_starmade_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_starmade_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /bfbc2.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Battlefield Bad Company 2 protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_BFBC2_H 11 | #define QSTAT_BFBC2_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_bfbc2_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_bfbc2_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /cube2.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Cube 2 / Sauerbraten protocol 6 | * Copyright 2011 NoisyB 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_CUBE2_H 11 | #define QSTAT_CUBE2_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_cube2_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_cube2_request_packet(struct qserver *server); 18 | 19 | #endif // QSTAT_CUBE2_H 20 | -------------------------------------------------------------------------------- /dirtybomb.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * DirtyBomb protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_DIRTYBOMB_H 11 | #define QSTAT_DIRTYBOMB_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_dirtybomb_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_dirtybomb_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /terraria.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Terraria / TShock protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_TERRARIA_H 11 | #define QSTAT_TERRARIA_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_terraria_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t send_terraria_request_packet(struct qserver *server); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /fl.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Frontlines-Fuel of War protocol 6 | * Copyright 2008 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_FL_H 11 | #define QSTAT_FL_H 12 | 13 | #include "qserver.h" 14 | 15 | query_status_t send_fl_request_packet(struct qserver *server); 16 | query_status_t send_fl_rule_request_packet(struct qserver *server); 17 | query_status_t deal_with_fl_packet(struct qserver *server, char *rawpkt, int pktlen); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /a2s.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * New Half-Life2 query protocol 6 | * Copyright 2005 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_A2S_H 11 | #define QSTAT_A2S_H 12 | 13 | #include "qserver.h" 14 | 15 | query_status_t send_a2s_request_packet(struct qserver *server); 16 | query_status_t send_a2s_rule_request_packet(struct qserver *server); 17 | query_status_t deal_with_a2s_packet(struct qserver *server, char *rawpkt, int pktlen); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /template/unrealTh.html: -------------------------------------------------------------------------------- 1 | $HTML 2 | 3 | Unreal Servers 4 | 5 | 6 | 7 | 8 | 9 |

Unreal Servers

10 | Last update: $NOW 11 | 12 |
13 |

Example of Unreal support in QStat.
14 |

15 |

16 | 17 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /armyops.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * 4 | * Armyops Protocol 5 | * Copyright 2005 Omnix 6 | * 7 | * Licensed under the Artistic License, see LICENSE.txt for license terms 8 | * 9 | */ 10 | 11 | #include "qstat.h" 12 | 13 | void 14 | json_display_armyops_player_info(struct qserver *server) 15 | { 16 | struct player *player; 17 | 18 | player = server->players; 19 | for ( ; player != NULL; player = player->next) { 20 | player->score = calculate_armyops_score(player); 21 | } 22 | 23 | json_display_player_info(server); 24 | } 25 | 26 | 27 | // vim: sw=4 ts=4 noet 28 | -------------------------------------------------------------------------------- /gs3.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * New Gamespy v3 query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_GS3_H 11 | #define QSTAT_GS3_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t deal_with_gs3_packet(struct qserver *server, char *pkt, int pktlen); 17 | query_status_t deal_with_gs3_status(struct qserver *server, char *rawpkt, int pktlen); 18 | query_status_t send_gs3_request_packet(struct qserver *server); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /template/tribes2th.html: -------------------------------------------------------------------------------- 1 | $HTML 2 | 3 | Tribes 2 Servers 4 | 5 | 6 | 7 | 8 | 9 |

Tribes 2 Servers

10 | Last update: $NOW 11 | 12 |
13 |

Example of Tribes 2 support in QStat.
14 |

15 |

16 | 17 |

Server Name 20 | Players 21 | Map 22 | Address 23 |
18 | 19 | 25 | -------------------------------------------------------------------------------- /ottd.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * 4 | * opentTTD protocol 5 | * Copyright 2007 Ludwig Nussel 6 | * 7 | * Licensed under the Artistic License, see LICENSE.txt for license terms 8 | */ 9 | #ifndef QSTAT_OTTD_H 10 | #define QSTAT_OTTD_H 11 | 12 | #include "qstat.h" 13 | 14 | query_status_t send_ottdmaster_request_packet(struct qserver *server); 15 | query_status_t deal_with_ottdmaster_packet(struct qserver *server, char *rawpkt, int pktlen); 16 | 17 | query_status_t send_ottd_request_packet(struct qserver *server); 18 | query_status_t deal_with_ottd_packet(struct qserver *server, char *rawpkt, int pktlen); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /template/brocTs.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 21 | -------------------------------------------------------------------------------- /ventrilo.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Ventrilo query protocol 6 | * Algorithm by Luigi Auriemma (Reversing and first version in c) 7 | * Copyright 2010 Michael Willigens 8 | * 9 | * Licensed under the Artistic License, see LICENSE.txt for license terms 10 | */ 11 | #ifndef QSTAT_VENTRILO_H 12 | #define QSTAT_VENTRILO_H 13 | 14 | #include "qserver.h" 15 | 16 | // Packet processing methods 17 | query_status_t deal_with_ventrilo_packet(struct qserver *server, char *pkt, int pktlen); 18 | query_status_t send_ventrilo_request_packet(struct qserver *server); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /tee.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Teeworlds protocol 6 | * Copyright 2008 ? Emiliano Leporati 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_TEE_H 11 | #define QSTAT_TEE_H 12 | 13 | #include "qserver.h" 14 | 15 | // Packet processing methods 16 | query_status_t send_teeserver_request_packet(struct qserver *server); 17 | query_status_t deal_with_teeserver_packet(struct qserver *server, char *rawpkt, int pktlen); 18 | query_status_t send_teemaster_request_packet(struct qserver *server); 19 | query_status_t deal_with_teemaster_packet(struct qserver *server, char *rawpkt, int pktlen); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Sun Feb 23 10:52:18 PST 2003 2 | 3 | Added $SCORE for player score 4 | Fixed XML escaping bugs 5 | Fixed repeated rule values from UT2003 servers 6 | Fixed excessive retries to UT2003 servers that set minplayers 7 | Fixed reported # players for UT2003 server that set minplayers 8 | Changed sof2m default protocol to 2004 (SOF 1.02) 9 | 10 | Tue Jan 7 16:24:57 PST 2003 11 | 12 | Fixed manual compile instructions in COMPILE.txt 13 | Updated RTCW master protocol from 58 to 60 14 | Added support for All-Seeing Eye protocol (-eye) 15 | [still need to write documentation] 16 | Fixed rare divide by zero displaying ping time 17 | Added information about UT2003 master server lists to info/UT2003.txt 18 | -------------------------------------------------------------------------------- /packet_manip.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Packet module 6 | * Copyright 2005 Steven Hartland based on code by Steve Jankowski 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | 11 | #ifndef QSTAT_PACKETS_H 12 | #define QSTAT_PACKETS_H 13 | 14 | #include "qstat.h" 15 | 16 | int combine_packets(struct qserver *server); 17 | int add_packet(struct qserver *server, unsigned int pkt_id, int pkt_index, int pkt_max, int datalen, char *data, int calc_max); 18 | int next_sequence(); 19 | SavedData *get_packet_fragment(int index); 20 | unsigned combined_length(struct qserver *server, int pkt_id); 21 | unsigned packet_count(struct qserver *server); 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /haze.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * New Haze query protocol 6 | * Copyright 2007 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_HAZE_H 11 | #define QSTAT_HAZE_H 12 | 13 | #include "qserver.h" 14 | 15 | #define HAZE_BASIC_INFO 0x01 16 | #define HAZE_GAME_RULES 0x02 17 | #define HAZE_PLAYER_INFO 0x04 18 | #define HAZE_TEAM_INFO 0x08 19 | 20 | // Packet processing methods 21 | query_status_t deal_with_haze_packet(struct qserver *server, char *pkt, int pktlen); 22 | query_status_t deal_with_haze_status(struct qserver *server, char *rawpkt, int pktlen); 23 | query_status_t send_haze_request_packet(struct qserver *server); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /template/unrealTs.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 22 | -------------------------------------------------------------------------------- /COMPILE.md: -------------------------------------------------------------------------------- 1 | # Compilation instructions for qStat 2 | 3 | ## Linux and other GNU systems 4 | 5 | ### Build 6 | ```shall 7 | ./configure && make 8 | ``` 9 | ### Install 10 | To install qstat run 11 | ```shell 12 | make install 13 | ``` 14 | ### Configuration 15 | To see which configuration parameters can be tweaked, run 16 | ```shell 17 | ./configure --help 18 | ``` 19 | 20 | If you want to compile from GIT you need to first install `autoconf` and `automake`, then run 21 | ```shell 22 | ./autogen.sh 23 | ``` 24 | 25 | ## Windows 26 | Release build 27 | ```shell 28 | nmake /f Makefile.noauto windows 29 | ``` 30 | Debug build 31 | ```shell 32 | nmake /f Makefile.noauto windows_debug 33 | ``` 34 | 35 | ## Solaris 36 | ```shell 37 | make -f Makefile.noauto solaris 38 | ``` 39 | 40 | ## HPUX 41 | ```shell 42 | make -f Makefile.noauto hpux 43 | ``` 44 | -------------------------------------------------------------------------------- /template/tribes2ts.html: -------------------------------------------------------------------------------- 1 | $(IFNOT:ISMASTER) 2 | 3 | 12 | 26 | $ENDIF 27 | -------------------------------------------------------------------------------- /template/brocTh.html: -------------------------------------------------------------------------------- 1 | $HTML 2 | 3 | broccoli Quake 2 servers 4 | 5 | 6 | 7 | 8 | 9 |

broccoli Quake 2 servers

10 | Last update: $NOW 11 | 12 |
13 |

The Solaris version of Quake II v 3.15 (soon 3.17) is not yet 14 | available. As soon as I can get my hands on the bits, I'll be running 15 | upgrading the server. 16 |

I run $TOTALSERVERS servers on a Sun Ultra 2 running Solaris 2.5.1. 17 | The machine has two CPUs, so performance is good even when I'm using 18 | it for work. Enjoy!
19 |

20 |

21 | 22 |

Server Name 20 | Players 21 | Mission 22 | Map 23 | Address 24 |
$SERVERNAME 3 | $(IFNOT:QWMASTER)$PLAYERS/$MAXPLAYERS$(ENDIF) 4 | $MAP 5 | $GAME 6 | $HOSTNAME 7 | $(IF:PLAYERS)$(IF:FLAG(-P)) 8 |
9 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | $PLAYERTEMPLATE 18 |
 Player NameFragsPing 
19 | $ENDIF$ENDIF 20 |
 
$SERVERNAME 3 | $(IFNOT:QWMASTER)$PLAYERS/$MAXPLAYERS$(ENDIF) 4 | $MAP 5 | $HOSTNAME 6 | $(IF:RULE(AdminEMail)) $(RULE:AdminEMail)$(ENDIF) 7 | $(IF:PLAYERS)$(IF:FLAG(-P)) 8 |
9 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | $PLAYERTEMPLATE 19 |
 Player NameFragsSkinTeam 
20 | $ENDIF$ENDIF 21 |
 
$SERVERNAME 4 | $PLAYERS/$MAXPLAYERS 5 | $RULE:mission 6 | $MAP 7 | $HOSTNAME 8 | $(IF:RULE(info))
$(RULE:info)
$(ENDIF) 9 | $(IF:PLAYERS)$(IF:FLAG(-P)) 10 |
11 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | $PLAYERTEMPLATE 23 |
 Player NameTypeTribeScore Team 
24 | $ENDIF$ENDIF 25 |
 
23 | 24 | 30 | -------------------------------------------------------------------------------- /template/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains the output templates used for the 2 | old broccoli server status page. 3 | 4 | ** The broccoli servers are no longer running. You should substitute 5 | ** your own server addresses into 'broc.lst'. 6 | 7 | brocTh.html header template 8 | brocTp.html player template 9 | brocTs.html server template 10 | brocTt.html trailer template 11 | broc.lst server list 12 | 13 | To generate an HTML page for the broccoli servers: 14 | 15 | qstat -P -f broc.lst -Ts brocTs.html -Th brocTh.html -Tt brocTt.html -Tp brocTp.html -sort g -of qservers.html 16 | 17 | The HTML output will be put in "qservers.html". 18 | 19 | 20 | 21 | I've also included some templates for Unreal/UT. They can be used 22 | like this: 23 | 24 | qstat -P -R -f unreal.lst -Th unrealTh.html -Tp unrealTp.html -Ts unrealTs.html -Tt unrealTt.html -sort g -of unreal.html 25 | 26 | The HTML output will be put in "unreal.html". 27 | 28 | 29 | qstat -P -R -f tribes2.lst -Th tribes2th.html -Tp tribes2tp.html -Ts tribes2ts.html -Tt tribes2tt.html -sort g -of tribes2.html 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build_linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: autogen 15 | run: ./autogen.sh 16 | - name: autoreconf force 17 | run: autoreconf --force 18 | - name: configure 19 | run: ./configure 20 | - name: make check 21 | run: make check 22 | - name: make distcheck 23 | run: make distcheck 24 | 25 | build_macos: 26 | runs-on: macos-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: install automake 30 | run: brew install automake 31 | - name: autogen 32 | run: ./autogen.sh 33 | - name: autoreconf force 34 | run: autoreconf --force 35 | - name: configure 36 | run: ./configure 37 | - name: make check 38 | run: make check 39 | 40 | build_windows: 41 | runs-on: windows-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | - uses: ilammy/msvc-dev-cmd@v1 45 | - name: nmake debug build 46 | run: | 47 | nmake -f Makefile.noauto windows_debug 48 | -------------------------------------------------------------------------------- /tests/etqws.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # simple responder for doom3/quake4/etqw queries 3 | 4 | use strict; 5 | use IO::Socket::INET; 6 | use IO::File; 7 | 8 | my $sock = IO::Socket::INET->new( 9 | LocalAddr => 'localhost', 10 | Proto => 'udp'); 11 | 12 | print $sock->sockport(),"\n"; 13 | 14 | my $rin = ''; 15 | vec($rin, $sock->fileno, 1) = 1; 16 | 17 | my @files = @ARGV; 18 | die "USAGE: $0 " unless @files; 19 | 20 | sub _stat_size($) 21 | { 22 | my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, 23 | $atime,$mtime,$ctime,$blksize,$blocks) = stat($_[0]); 24 | return $size; 25 | } 26 | 27 | sub readfile($) 28 | { 29 | my $fn = shift; 30 | print "reading $fn\n"; 31 | my $f = IO::File->new($fn, "r"); 32 | my $buf; 33 | $f->read($buf, _stat_size($f)); 34 | $f->close; 35 | return $buf; 36 | } 37 | 38 | while (select(my $rout = $rin, undef, undef, undef)) { 39 | my $data = ''; 40 | my $hispaddr; 41 | $hispaddr = $sock->recv($data, 0xffff, 0) || die "recv: $!"; 42 | my ($port, $hisiaddr) = sockaddr_in($hispaddr); 43 | printf '%d bytes from %s:%d'."\n", length($data), inet_ntoa($hisiaddr), $port; 44 | 45 | if($data !~ /^\xff\xffgetInfo/) { 46 | printf 'invalid packet from %s:%d'."\n", inet_ntoa($hisiaddr), $port; 47 | next; 48 | } 49 | 50 | my $buf = readfile($files[0]); 51 | 52 | $sock->send($buf, 0, $hispaddr); 53 | } 54 | -------------------------------------------------------------------------------- /doom3.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Doom3 / Quake4 protocol 6 | * Copyright 2005 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_DOOM3_H 11 | #define QSTAT_DOOM3_H 12 | 13 | #define DOOM3_DEFAULT_PORT 27666 14 | #define DOOM3_MASTER_DEFAULT_PORT 27650 15 | 16 | #define QUAKE4_DEFAULT_PORT 28004 17 | #define QUAKE4_MASTER_DEFAULT_PORT 27650 18 | 19 | #define PREY_DEFAULT_PORT 27719 20 | #define PREY_MASTER_DEFAULT_PORT 27655 21 | 22 | #define ETQW_DEFAULT_PORT 27733 23 | 24 | #define WOLF_DEFAULT_PORT 27758 25 | 26 | query_status_t send_doom3master_request_packet(struct qserver *server); 27 | query_status_t deal_with_doom3master_packet(struct qserver *server, char *rawpkt, int pktlen); 28 | 29 | query_status_t deal_with_doom3_packet(struct qserver *server, char *rawpkt, int pktlen); 30 | 31 | query_status_t send_quake4master_request_packet(struct qserver *server); 32 | query_status_t deal_with_quake4_packet(struct qserver *server, char *rawpkt, int pktlen); 33 | 34 | query_status_t deal_with_prey_packet(struct qserver *server, char *rawpkt, int pktlen); 35 | 36 | query_status_t deal_with_etqw_packet(struct qserver *server, char *rawpkt, int pktlen); 37 | 38 | query_status_t deal_with_wolf_packet(struct qserver *server, char *rawpkt, int pktlen); 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /contrib.cfg: -------------------------------------------------------------------------------- 1 | # QStat config file 2 | # 3 | # The following game types were contributed by QStat users. 4 | # To use, copy them into your qstat.cfg or add "-f contrib.cfg" to you 5 | # qstat command line. 6 | 7 | gametype aps new extend gps 8 | name = Alien vs Predator 2 9 | default port = 27888 10 | template var = AVP2 11 | game rule = gamename 12 | end 13 | 14 | gametype uts new extend uns 15 | name = Unreal Tournament 16 | default port = 7777 17 | template var = UNREALTOURNAMENT 18 | game rule = gametype 19 | end 20 | 21 | gametype sms new extend gps 22 | name = Serious Sam 23 | default port = 25601 24 | template var = SERIOUSSAM 25 | game rule = gametype 26 | end 27 | 28 | gametype gos new extend gps 29 | name = Global Operations 30 | default port = 28672 31 | template var = GLOBALOPS 32 | game rule = gamedir 33 | end 34 | 35 | gametype j2s new extend q3s 36 | name = Jedi Knight 2 37 | default port = 28070 38 | template var = JEDIKNIGHT2 39 | game rule = gamename 40 | end 41 | 42 | gametype dks new extend gps 43 | name = Daikatana 44 | default port = 27992 45 | template var = DAIKATANA 46 | game rule = gamedir 47 | end 48 | 49 | gametype vcs new extend gps 50 | name = V8 Supercar Challange 51 | default port = 16700 52 | template var = V8SUPERCAR 53 | game rule = gamedir 54 | end 55 | 56 | -------------------------------------------------------------------------------- /tests/xml.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # simple qstat xml output parser, prints values specified as command 3 | # line arguments 4 | # 5 | # Author: Ludwig Nussel 6 | # 7 | # Usage Examples: 8 | # 9 | # Print server name: 10 | # "qstat/server/name" 11 | # 12 | # Print second server rule (numbered from zero): 13 | # "qstat/server/rules/rule/1" 14 | # 15 | # Print the rule with name "gamename": 16 | # "qstat/server/rules/rule/[name=gamename]" 17 | # 18 | # Print name of sixth player: 19 | # "qstat/server/players/player/5/name" 20 | # 21 | # Print clan of player with name "suCk3r": 22 | # "qstat/server/players/player/[name=suCk3r]/clan" 23 | 24 | use strict; 25 | use XML::Bare; 26 | use Data::Dumper; 27 | 28 | sub getvalue { 29 | my $x = shift; 30 | my @a = split(/\//, shift); 31 | for my $n (@a) { 32 | if ($n =~ /^[[:digit:]]+$/) { 33 | return undef unless exists $x->[$n]; 34 | $x = $x->[$n]; 35 | } elsif ($n =~ /\[(.*)=(.*)\]/) { 36 | my ($k, $v) = ($1, $2); 37 | my $r; 38 | for my $i (@$x) { 39 | next unless exists $i->{$k}; 40 | if($i->{$k}->{value} eq $v) { 41 | $r = $i; 42 | last; 43 | } 44 | } 45 | return undef unless $r; 46 | $x = $r; 47 | } else { 48 | return undef unless exists $x->{$n}; 49 | $x = $x->{$n}; 50 | } 51 | } 52 | return $x->{value}; 53 | } 54 | 55 | sub printvalue { 56 | my $val = getvalue(@_); 57 | $val = "(undefined)" unless defined $val; 58 | print $val, "\n" 59 | } 60 | 61 | my $xml = XML::Bare->new(text => join('', ))->parse(); 62 | 63 | for (@ARGV) { 64 | printvalue($xml, $_); 65 | } 66 | -------------------------------------------------------------------------------- /scripts/gen-version.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Default file paramters 4 | SETLOCAL EnableDelayedExpansion 5 | SET "VERSION_FILE=.version" 6 | SET "TEMPLATE_FILE=version.h.tmpl" 7 | SET "HEADER_FILE=version.h" 8 | 9 | IF NOT [%1] == [] ( 10 | SET "VERSION_FILE=%1" 11 | ) 12 | IF NOT [%2] == [] ( 13 | SET "TEMPLATE_FILE=%2" 14 | ) 15 | IF NOT [%3] == [] ( 16 | SET "HEADER_FILE=%3" 17 | ) 18 | 19 | IF [!QSTAT_VERSION!] == [] ( 20 | :: QSTAT_VERSION not set determine from git. 21 | FOR /F "usebackq" %%v IN (`cmd /c "git status --porcelain | grep -Fv 'gnuconfig.h.in~' | grep -E '^(M^| M^|\?\?)' | wc -l | sed -e 's/^[[:space:]]*//'"`) DO ( 22 | IF NOT %%v == 0 ( 23 | :: We have modifications so indicate so in the version. 24 | SET "QSTAT_VERSION=!QSTAT_VERSION!-modified" 25 | ) 26 | ) 27 | ) 28 | 29 | IF EXIST "!VERSION_FILE!" ( 30 | :: .version file exists load the value. 31 | set /p OLD_VERSION=<"!VERSION_FILE!" 32 | ) 33 | 34 | IF NOT [!OLD_VERSION!] == [!QSTAT_VERSION!] ( 35 | :: version has changed update the file. 36 | echo|set /p="!QSTAT_VERSION!" > "!VERSION_FILE!" 37 | ) 38 | 39 | :: Update version.h if needed. 40 | IF NOT EXIST "!HEADER_FILE!" ( 41 | :: File doesn't exist to just create. 42 | sed "s/CHANGEME/!QSTAT_VERSION!/g" "!TEMPLATE_FILE!" > "!HEADER_FILE!" 43 | ) else ( 44 | :: File exists compare with new. 45 | SET "TMP_FILE=!HEADER_FILE!.tmp" 46 | 47 | sed "s/CHANGEME/!QSTAT_VERSION!/g" "!TEMPLATE_FILE!" > "!TMP_FILE!" 48 | 49 | :: cmp doesn't correctly set ERRORLEVEL so we use redirection. 50 | cmp -s "!HEADER_FILE!" "!TMP_FILE!" && rm -f "!TMP_FILE!" || mv "!TMP_FILE!" "!HEADER_FILE!" 51 | ) 52 | 53 | ENDLOCAL 54 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Utility Functions 3 | * Copyright 2012 Steven Hartland 4 | * 5 | * Licensed under the Artistic License, see LICENSE.txt for license terms 6 | */ 7 | #ifndef QSTAT_UTILS_H 8 | #define QSTAT_UTILS_H 9 | 10 | #ifndef _WIN32 11 | #include 12 | #include 13 | #ifdef HAVE_CONFIG_H 14 | #include "gnuconfig.h" 15 | #endif 16 | #endif 17 | 18 | // BSD has strnstr 19 | #if defined(__FreeBSD__) || defined(__MidnightBSD__) 20 | #ifndef HAVE_STRNSTR 21 | #define HAVE_STRNSTR 1 22 | #endif /* HAVE_STRNSTR */ 23 | #endif 24 | 25 | #ifndef _WIN32 26 | #ifndef HAVE_ERR_H 27 | #define HAVE_ERR_H 1 28 | #endif /* HAVE_ERR */ 29 | #endif 30 | 31 | #if !HAVE_STRNSTR 32 | #include 33 | char *qstat_strnstr(const char *s, const char *find, size_t slen); 34 | 35 | #define strnstr(s, find, slen) qstat_strnstr(s, find, slen) 36 | #endif 37 | 38 | #if !HAVE_STRNDUP 39 | #include 40 | char *strndup(const char *string, size_t len); 41 | 42 | #endif 43 | 44 | #ifndef EX_OSERR 45 | #define EX_OSERR 71 /* system error (e.g., can't fork) */ 46 | #endif 47 | 48 | #ifndef EX_SOFTWARE 49 | #define EX_SOFTWARE 70 /* An internal software error has been detected */ 50 | #endif 51 | 52 | #if !HAVE_ERR_H 53 | void err(int eval, const char *fmt, ...); 54 | void warn(const char *fmt, ...); 55 | 56 | #endif 57 | 58 | #if defined(_MSC_VER) && _MSC_VER < 1600 59 | typedef __int8 int8_t; 60 | typedef unsigned __int8 uint8_t; 61 | typedef __int16 int16_t; 62 | typedef unsigned __int16 uint16_t; 63 | typedef __int32 int32_t; 64 | typedef unsigned __int32 uint32_t; 65 | typedef __int64 int64_t; 66 | typedef unsigned __int64 uint64_t; 67 | #elif defined(_MSC_VER) // && _MSC_VER >= 1600 68 | #include 69 | #else 70 | #include 71 | #endif 72 | 73 | char *str_replace(char *, char *, char *); 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Global variables... 4 | AUTOCONF="autoconf" 5 | AUTOHEADER="autoheader" 6 | AUTOM4TE="autom4te" 7 | AUTOMAKE="automake" 8 | ACLOCAL="aclocal" 9 | 10 | # Please add higher versions first. The last version number is the minimum 11 | # needed to compile KDE. Do not forget to include the name/version # 12 | # separator if one is present, e.g. -1.2 where - is the separator. 13 | AUTOCONF_VERS="-2.61 261 -2.59 259 -2.58 258 -2.57 257 -2.54 254 -2.53 253" 14 | AUTOMAKE_VERS="-1.9 19 -1.8 18 -1.7 17 -1.6 16 -1.5 15" 15 | 16 | # We don't use variable here for remembering the type ... strings. Local 17 | # variables are not that portable, but we fear namespace issues with our 18 | # includer. 19 | check_autoconf() 20 | { 21 | echo "Checking autoconf version..." 22 | for ver in $AUTOCONF_VERS; do 23 | if test -x "`$WHICH $AUTOCONF$ver 2>/dev/null`"; then 24 | AUTOCONF="`$WHICH $AUTOCONF$ver`" 25 | AUTOHEADER="`$WHICH $AUTOHEADER$ver`" 26 | AUTOM4TE="`$WHICH $AUTOM4TE$ver`" 27 | break 28 | fi 29 | done 30 | } 31 | 32 | check_automake() 33 | { 34 | echo "Checking automake version..." 35 | for ver in $AUTOMAKE_VERS; do 36 | if test -x "`$WHICH $AUTOMAKE$ver 2>/dev/null`"; then 37 | AUTOMAKE="`$WHICH $AUTOMAKE$ver`" 38 | ACLOCAL="`$WHICH $ACLOCAL$ver`" 39 | break 40 | fi 41 | done 42 | 43 | if test -n "$UNSERMAKE"; then 44 | AUTOMAKE="$UNSERMAKE" 45 | fi 46 | } 47 | 48 | check_which() 49 | { 50 | WHICH="" 51 | for i in "type -p" "which" "type" ; do 52 | T=`$i sh 2> /dev/null` 53 | test -x "$T" && WHICH="$i" && break 54 | done 55 | } 56 | 57 | check_which 58 | check_autoconf 59 | check_automake 60 | 61 | export AUTOCONF AUTOHEADER AUTOM4TE AUTOMAKE ACLOCAL 62 | 63 | set -e 64 | 65 | echo "Running aclocal..." 66 | $ACLOCAL 67 | echo "Running autoconf..." 68 | $AUTOCONF 69 | echo "Running autoheader..." 70 | $AUTOHEADER 71 | echo "Running automake..." 72 | $AUTOMAKE -a --foreign 73 | -------------------------------------------------------------------------------- /mumble.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Mumble protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #ifndef _WIN32 16 | #include 17 | #endif 18 | 19 | #include "debug.h" 20 | #include "qstat.h" 21 | #include "packet_manip.h" 22 | 23 | query_status_t 24 | send_mumble_request_packet(struct qserver *server) 25 | { 26 | return (send_packet(server, server->type->status_packet, server->type->status_len)); 27 | } 28 | 29 | 30 | query_status_t 31 | deal_with_mumble_packet(struct qserver *server, char *rawpkt, int pktlen) 32 | { 33 | // skip unimplemented ack, crc, etc 34 | char *pkt = rawpkt; 35 | char bandwidth[11]; 36 | char version[12]; 37 | 38 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 39 | if ((24 != pktlen) || (0 != memcmp(pkt + 4, server->type->status_packet + 4, 8))) { 40 | // unknown packet 41 | return (PKT_ERROR); 42 | } 43 | 44 | // version 45 | server->protocol_version = ntohl(*(unsigned long *)pkt); 46 | sprintf(version, "%u.%u.%u", (unsigned char)*pkt + 1, (unsigned char)*pkt + 2, (unsigned char)*pkt + 3); 47 | add_rule(server, "version", version, NO_FLAGS); 48 | pkt += 4; 49 | 50 | // ident 51 | pkt += 8; 52 | 53 | // num players 54 | server->num_players = ntohl(*(unsigned long *)pkt); 55 | pkt += 4; 56 | 57 | // max players 58 | server->max_players = ntohl(*(unsigned long *)pkt); 59 | pkt += 4; 60 | 61 | // allowed bandwidth 62 | sprintf(bandwidth, "%d", ntohl(*(unsigned long *)pkt)); 63 | add_rule(server, "allowed_bandwidth", bandwidth, NO_FLAGS); 64 | pkt += 4; 65 | 66 | // Unknown details 67 | server->map_name = strdup("N/A"); 68 | server->server_name = strdup("Unknown"); 69 | add_rule(server, "gametype", "Unknown", NO_FLAGS); 70 | 71 | return (DONE_FORCE); 72 | } 73 | -------------------------------------------------------------------------------- /debug.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * debug helper functions 6 | * Copyright 2004 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_DEBUG_H 11 | #define QSTAT_DEBUG_H 12 | 13 | #include 14 | 15 | #include "qstat.h" 16 | 17 | #ifdef DEBUG 18 | #include 19 | #ifdef _WIN32 20 | void _debug(const char *file, int line, const char *function, int level, const char *fmt, ...); 21 | 22 | #define debug(level, fmt, ...) \ 23 | if (level <= get_debug_level()) \ 24 | _debug(__FILE__, __LINE__, __FUNCTION__, level, fmt, __VA_ARGS__) 25 | #else 26 | void _debug(const char *file, int line, const char *function, int level, const char *fmt, ...) 27 | GCC_FORMAT_PRINTF(5, 6); 28 | 29 | #define debug(level, fmt, rem ...) \ 30 | if (level <= get_debug_level()) \ 31 | _debug(__FILE__, __LINE__, __FUNCTION__, level, fmt, ## rem) 32 | #endif // _WIN32 33 | #else 34 | #define debug(...) 35 | #endif // DEBUG 36 | 37 | void dump_packet(const char *buf, int buflen); 38 | 39 | #ifdef ENABLE_DUMP 40 | #ifndef _WIN32 41 | #include 42 | #include 43 | #else 44 | #ifdef _MSC_VER 45 | #define ssize_t SSIZE_T 46 | #define uint32_t UINT32 47 | #endif 48 | #endif 49 | #include 50 | #include 51 | extern int do_dump; 52 | ssize_t send_dump(int s, const void *buf, size_t len, int flags); 53 | 54 | #ifndef QSTAT_DEBUG_C 55 | #define send(s, buf, len, flags) send_dump(s, buf, len, flags) 56 | #endif 57 | #endif 58 | 59 | /** report a packet decoding error to stderr */ 60 | void malformed_packet(const struct qserver *server, const char *fmt, ...) GCC_FORMAT_PRINTF(2, 3); 61 | 62 | int get_debug_level(void); 63 | void set_debug_level(int level); 64 | 65 | void print_packet(struct qserver *server, const char *buf, int buflen); 66 | void output_packet(struct qserver *server, const char *buf, int buflen, int to); 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = template info 2 | 3 | CFLAGS = -Dsysconfdir=\"$(sysconfdir)\" @CFLAGS@ 4 | 5 | .PHONY: $(builddir)/.compiler_flags 6 | $(builddir)/.compiler_flags: 7 | echo '$(CFLAGS)' | cmp -s - $@ || echo '$(CFLAGS)' > $@ 8 | 9 | .PHONY: $(builddir)/.version 10 | $(builddir)/.version: 11 | ver=$$($(srcdir)/scripts/version.sh); \ 12 | echo $$ver | cmp -s - $@ || echo $$ver > $@ 13 | 14 | $(builddir)/version.h: $(builddir)/.version $(srcdir)/version.h.tmpl 15 | ver=$$(cat $<); \ 16 | file=$$(sed "s/CHANGEME/$$ver/g" $(srcdir)/version.h.tmpl); \ 17 | echo "$$file" | cmp -s - $@ || echo "$$file" > $@ 18 | 19 | clean-local: 20 | -rm -f $(builddir)/.compiler_flags $(builddir)/.version $(builddir)/version.h 21 | 22 | distclean-local: clean-local 23 | 24 | BUILT_SOURCES = \ 25 | .compiler_flags \ 26 | .version \ 27 | version.h 28 | 29 | bin_PROGRAMS = qstat 30 | 31 | qstat_DEPENDENCIES = \ 32 | $(builddir)/.compiler_flags 33 | 34 | qstat_SOURCES = \ 35 | version.h.tmpl \ 36 | version.h \ 37 | xform.c xform.h \ 38 | config.c config.h \ 39 | debug.c debug.h \ 40 | utils.c utils.h \ 41 | hcache.c \ 42 | md5.c md5.h \ 43 | qserver.c qserver.h \ 44 | qstat.c qstat.h \ 45 | template.c \ 46 | display_json.c display_json.h \ 47 | a2s.c a2s.h \ 48 | packet_manip.c packet_manip.h \ 49 | ut2004.c ut2004.h \ 50 | doom3.c doom3.h \ 51 | gps.c gps.h \ 52 | gs2.c gs2.h \ 53 | gs3.c gs3.h \ 54 | ts2.c ts2.h \ 55 | tm.c tm.h \ 56 | haze.c haze.h \ 57 | ottd.c ottd.h \ 58 | wic.c wic.h \ 59 | fl.c fl.h \ 60 | tee.c tee.h \ 61 | cube2.c cube2.h \ 62 | ts3.c ts3.h \ 63 | bfbc2.c bfbc2.h \ 64 | ventrilo.c ventrilo.h \ 65 | mumble.c mumble.h \ 66 | terraria.c terraria.h \ 67 | crysis.c crysis.h \ 68 | dirtybomb.c dirtybomb.h \ 69 | starmade.c starmade.h \ 70 | farmsim.c farmsim.h \ 71 | ksp.c ksp.h \ 72 | tf.c tf.h \ 73 | armyops.c armyops.h 74 | 75 | dist_configfiles_DATA = qstat.cfg 76 | configfilesdir = $(sysconfdir) 77 | 78 | EXTRA_DIST = CHANGES.txt COMPILE.md LICENSE.txt \ 79 | Makefile.noauto \ 80 | ChangeLog \ 81 | qstatdoc.html \ 82 | contrib.cfg \ 83 | scripts/version.sh 84 | -------------------------------------------------------------------------------- /info/UT2003.txt: -------------------------------------------------------------------------------- 1 | Unreal Tournament 2003 servers can be queried with the Gamespy style 2 | protocol (-gps) or with the native UT2003 protocol (-ut2s). The 3 | query port offset for Gamespy is usually 10. The query port offset 4 | for the native UT2003 protocol is always 1. 5 | 6 | The gamespy response returns a team name for each player, the UT2S 7 | response does not. Don't be concerned because the team name is the 8 | same for all players, and seems to be map name followed by the string 9 | ".xTeamRoster" Probably a bug in the UT2003 gamespy support. 10 | 11 | The UT2S response includes a player global statistics id. The gamespy 12 | response does not. However, this id is zero for all players on both 13 | demo and retail servers. I guess they don't have global stats implemented. 14 | 15 | The protocols return similar but different servers rules. In UT2S the 16 | rule names are all lower-case. In gamespy, they are mixed-case. Some of 17 | the rules overlap, but each returns info not available from the other 18 | protocol. 19 | 20 | In the UT2S response, the "Mutator" rule (the only one with mixed-case) 21 | may appear multiple times. If you use $(RULE:Mutator) only the value of 22 | the first Mutator will be output. 23 | 24 | UT2003 servers frequently do not return information for all of the 25 | players. I don't know why. 26 | 27 | UT2003 master server lists are available from Epic Games. Here's the 28 | description from the Unreal Technology page: 29 | --------------------- 30 | We have made server lists available via HTTP for both demo and full version 31 | UT2003 servers so that 3rd party server query tools can add UT2003 support. 32 | 33 | http://ut2003master.epicgames.com/serverlist/full-all.txt 34 | http://ut2003master.epicgames.com/serverlist/demo-all.txt 35 | 36 | These URLs contain a tab-separated list of server IP, game port and 37 | query port for all servers in our master server. The query port is the 38 | port number the server is listening on for UDP queries. The query format 39 | and response is exactly the same as UT 1. The source code to the game 40 | server's query responder is in the UdpGameSpyQuery UnrealScript class. 41 | --------------------- 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Tag Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build_linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: autogen 14 | run: ./autogen.sh 15 | - name: configure 16 | run: ./configure 17 | - name: make 18 | run: QSTAT_VERSION=${{ github.event.release.tag_name }} make 19 | - uses: actions/upload-artifact@v2 20 | with: 21 | name: linux_amd64 22 | path: qstat 23 | 24 | build_macos: 25 | runs-on: macos-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: install automake 29 | run: brew install automake 30 | - name: autogen 31 | run: ./autogen.sh 32 | - name: configure 33 | run: ./configure 34 | - name: make 35 | run: QSTAT_VERSION=${{ github.event.release.tag_name }} make 36 | - uses: actions/upload-artifact@v2 37 | with: 38 | name: darwin_amd64 39 | path: qstat 40 | 41 | 42 | build_windows: 43 | runs-on: windows-latest 44 | env: 45 | QSTAT_VERSION: ${{ github.event.release.tag_name }} 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: ilammy/msvc-dev-cmd@v1 49 | - name: nmake 50 | run: | 51 | nmake -f Makefile.noauto windows windows_debug 52 | - uses: actions/upload-artifact@v2 53 | with: 54 | name: windows_amd64 55 | path: qstat.exe 56 | # TODO(austin) build for mac-os which would cover freebsd 57 | # https://github.com/vmactions/freebsd-vm 58 | release: 59 | runs-on: ubuntu-latest 60 | needs: 61 | - build_windows 62 | - build_linux 63 | - build_macos 64 | steps: 65 | - uses: actions/checkout@v2 66 | - uses: actions/download-artifact@v2 67 | with: 68 | path: bin 69 | - name: zip 70 | run: zip -r release.zip bin/* qstat.cfg contrib.cfg LICENSE.* 71 | - name: Release 72 | uses: "softprops/action-gh-release@v1" 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | prerelease: false 77 | tag_name: "${{ github.event.release.tag_name }}" 78 | files: | 79 | release.zip 80 | -------------------------------------------------------------------------------- /display_json.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * steve@qstat.org 5 | * http://www.qstat.org 6 | * 7 | * Inspired by QuakePing by Len Norton 8 | * 9 | * JSON output 10 | * Contributed by Steve Teuber 11 | * 12 | * Copyright 2013 by Steve Teuber 13 | * 14 | * Licensed under the Artistic License, see LICENSE.txt for license terms 15 | */ 16 | #ifndef QSTAT_DISPLAY_JSON_H 17 | #define QSTAT_DISPLAY_JSON_H 18 | 19 | #include "qstat.h" 20 | #include "qserver.h" 21 | 22 | extern int json_display; 23 | extern int json_encoding; 24 | extern int json_printed; 25 | 26 | void json_footer(); 27 | void json_header(); 28 | void json_protocols(); 29 | void json_version(); 30 | 31 | char *json_escape(char *string); 32 | 33 | void json_display_server(struct qserver *server); 34 | void json_display_server_rules(struct qserver *server); 35 | 36 | void json_display_bfbc2_player_info(struct qserver *server); 37 | void json_display_bfris_player_info(struct qserver *server); 38 | void json_display_descent3_player_info(struct qserver *server); 39 | void json_display_doom3_player_info(struct qserver *server); 40 | void json_display_eye_player_info(struct qserver *server); 41 | void json_display_farcry_player_info(struct qserver *server); 42 | void json_display_fl_player_info(struct qserver *server); 43 | void json_display_ghostrecon_player_info(struct qserver *server); 44 | void json_display_halflife_player_info(struct qserver *server); 45 | void json_display_player_info_info(struct player *player); 46 | void json_display_player_info(struct qserver *server); 47 | void json_display_q2_player_info(struct qserver *server); 48 | void json_display_q_player_info(struct qserver *server); 49 | void json_display_qw_player_info(struct qserver *server); 50 | void json_display_ravenshield_player_info(struct qserver *server); 51 | void json_display_savage_player_info(struct qserver *server); 52 | void json_display_starmade_player_info(struct qserver *server); 53 | void json_display_tee_player_info(struct qserver *server); 54 | void json_display_tm_player_info(struct qserver *server); 55 | void json_display_tribes2_player_info(struct qserver *server); 56 | void json_display_tribes_player_info(struct qserver *server); 57 | void json_display_ts2_player_info(struct qserver *server); 58 | void json_display_ts3_player_info(struct qserver *server); 59 | void json_display_unreal_player_info(struct qserver *server); 60 | void json_display_ventrilo_player_info(struct qserver *server); 61 | void json_display_wic_player_info(struct qserver *server); 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_INIT([qstat], m4_esyscmd([./scripts/version.sh | tr -d '\n']),[https://github.com/multiplay/qstat/issues]) 2 | AC_CONFIG_SRCDIR([qstat.c]) 3 | AM_CONFIG_HEADER([gnuconfig.h]) 4 | 5 | AC_PREREQ(2.53) 6 | AC_CANONICAL_HOST 7 | AM_INIT_AUTOMAKE([1.6 foreign]) 8 | 9 | dnl Checks for programs. 10 | AC_PROG_CC 11 | 12 | dnl Checks for header files. 13 | AC_HEADER_STDC 14 | 15 | AC_CHECK_HEADER(sys/mman.h, [have_mman_h=yes]) 16 | 17 | case $host in 18 | *mingw32*) 19 | AC_MSG_NOTICE([compiling for $host, adding -lwsock32]) 20 | LIBS="$LIBS -lwsock32" 21 | ;; 22 | esac 23 | 24 | AC_CHECK_FUNCS([strndup], [HAVE_STRNDUP=yes], [HAVE_STRNDUP=no]) 25 | AM_CONDITIONAL(NEED_STRNDUP, [test x$HAVE_STRNDUP = xno]) 26 | 27 | dnl Check for strnstr including broken one on MacOSX 10.4 which crashes 28 | dnl 29 | AC_CACHE_CHECK(for strnstr, ac_cv_func_strnstr, 30 | AC_TRY_RUN([ 31 | #include 32 | #include 33 | #include 34 | // we expect this to succeed, or crash on over-run. 35 | // if it passes otherwise we may need a better check. 36 | int main(int argc, char **argv) 37 | { 38 | int size = 20; 39 | char *str = malloc(size); 40 | memset(str, 'x', size); 41 | strnstr(str, "fubar", size); 42 | return 0; 43 | } 44 | ],ac_cv_func_strnstr="yes",ac_cv_func_strnstr="no") 45 | ) 46 | if test "$ac_cv_func_strnstr" = "yes" ; then 47 | AC_DEFINE(HAVE_STRNSTR,1,[Working strnstr]) 48 | else 49 | AC_DEFINE(HAVE_STRNSTR,0,[No or broken strnstr]) 50 | fi 51 | 52 | dnl check if user wants debug 53 | AC_MSG_CHECKING([whether to enable optimization]) 54 | AC_ARG_ENABLE(optimize,[ --disable-optimize turn off optimization]) 55 | if test x$enable_optimize != xno; then 56 | AC_MSG_RESULT([yes]) 57 | else 58 | CPPFLAGS="" 59 | CFLAGS="-ggdb" 60 | AC_MSG_RESULT([no]) 61 | fi 62 | 63 | dnl check if user wants debug 64 | AC_MSG_CHECKING([whether to enable debug output]) 65 | AC_ARG_ENABLE(debug,[ --disable-debug turn off debugging code]) 66 | if test x$enable_debug != xno; then 67 | CPPFLAGS="$CPPFLAGS -DDEBUG" 68 | AC_MSG_RESULT([yes]) 69 | else 70 | AC_MSG_RESULT([no]) 71 | fi 72 | 73 | AC_MSG_CHECKING([whether to enable packet dumps]) 74 | AC_ARG_ENABLE(dump,[ --enable-dump enable packet dumps]) 75 | if test x$enable_dump != xno; then 76 | if test x$have_mman_h = xyes; then 77 | CPPFLAGS="$CPPFLAGS -DENABLE_DUMP" 78 | AC_MSG_RESULT([yes]) 79 | else 80 | AC_MSG_RESULT([no, sys/mman.h missing]) 81 | fi 82 | else 83 | AC_MSG_RESULT([no]) 84 | fi 85 | 86 | AC_ARG_WITH(efence, 87 | [ --with-efence= Use electric fence for malloc debugging.], 88 | if test x$withval != xyes ; then 89 | LDFLAGS="${LDFLAGS} -L$withval" 90 | fi 91 | AC_CHECK_LIB(efence,malloc) 92 | ) 93 | 94 | dnl Use -Wall if we have gcc. 95 | changequote(,)dnl 96 | if test "x$GCC" = "xyes"; then 97 | case " $CFLAGS " in 98 | *[\ \ ]-Wall[\ \ ]*) ;; 99 | *) CFLAGS="$CFLAGS -Wall" ;; 100 | esac 101 | fi 102 | changequote([,])dnl 103 | 104 | AC_CONFIG_FILES([ 105 | Makefile 106 | template/Makefile 107 | info/Makefile 108 | ]) 109 | AC_OUTPUT 110 | -------------------------------------------------------------------------------- /Makefile.noauto: -------------------------------------------------------------------------------- 1 | ## Uncomment if you have gcc 2 | #CC = gcc 3 | #CFLAGS = -Wall -g -O2 4 | #LDFLAGS = 5 | #LDLIBS = 6 | 7 | #CFLAGS += -Dsysconfdir=\"/etc\" 8 | CFLAGS = -DDEBUG=1 -DENABLE_DUMP=1 9 | 10 | ## NOTE: if you get errors when linking qstat (missing symbols or 11 | ## libraries), then modify LDFLAGS or LDLIBS 12 | 13 | SRC = \ 14 | utils.c \ 15 | xform.c \ 16 | config.c \ 17 | debug.c \ 18 | hcache.c \ 19 | md5.c \ 20 | qserver.c \ 21 | qstat.c \ 22 | template.c \ 23 | display_json.c \ 24 | ut2004.c \ 25 | a2s.c \ 26 | packet_manip.c \ 27 | gs3.c \ 28 | gs2.c \ 29 | gps.c \ 30 | ts2.c \ 31 | doom3.c \ 32 | tm.c \ 33 | haze.c \ 34 | wic.c \ 35 | ottd.c \ 36 | fl.c \ 37 | tee.c \ 38 | ts3.c \ 39 | bfbc2.c \ 40 | ventrilo.c \ 41 | cube2.c \ 42 | mumble.c \ 43 | terraria.c \ 44 | crysis.c \ 45 | dirtybomb.c \ 46 | starmade.c \ 47 | farmsim.c \ 48 | ksp.c \ 49 | tf.c \ 50 | armyops.c 51 | 52 | SOURCES = \ 53 | version.h \ 54 | $(SRC) 55 | 56 | OBJ = $(SRC:.c=.obj) 57 | O = $(SRC:.c=.o) 58 | 59 | SOLARIS_LIBS = -lsocket -lnsl 60 | WINDOWS_LIBS = wsock32.lib 61 | OS2_LIBS = so32dll.lib tcp32dll.lib 62 | EMX_LIBS = -lsocket 63 | 64 | ifdef MAKEDIR: # gmake: false; nmake: unused target 65 | !ifdef MAKEDIR # gmake: not seen; nmake: true 66 | # nmake specific code 67 | 68 | LINK = link.exe 69 | LFLAGS = /nologo 70 | CFLAGS = $(CFLAGS) /nologo 71 | PHONY = .phony 72 | 73 | .phony: 74 | 75 | .version: $(PHONY) 76 | call scripts/gen-version.cmd 77 | 78 | version.h: .version 79 | 80 | !else # and now the other 81 | else 82 | # gnu make targets 83 | .PHONY: .compiler_flags 84 | .compiler_flags: 85 | echo '$(CFLAGS)' | cmp -s - $@ || echo '$(CFLAGS)' > $@ 86 | 87 | .PHONY: .version 88 | .version: 89 | @ver=$$(./scripts/version.sh); \ 90 | echo "$$ver" | cmp -s - $@ || echo "$$ver" > $@ 91 | 92 | version.h: .version 93 | @ver=$$(cat .version); \ 94 | file=$$(sed "s/CHANGEME/$$ver/g" ./version.h.tmpl); \ 95 | echo "$$file" | cmp -s - $@ || echo "$$file" > $@ 96 | 97 | endif # gmake: close condition; nmake: not seen 98 | !endif : # gmake: unused target; nmake close conditional 99 | 100 | all: qstat 101 | 102 | qstat: .compiler_flags version.h $(O) 103 | $(CC) $(CFLAGS) -o qstat $(O) $(LDFLAGS) $(LDLIBS) 104 | 105 | solaris: .compiler_flags $(SOURCES) 106 | $(CC) $(CFLAGS) -o qstat $(SRC) $(LDFLAGS) $(LDLIBS) $(SOLARIS_LIBS) 107 | 108 | aix sgi freebsd macosx osx openbsd irix linux: qstat 109 | 110 | hp hpux: .compiler_flags $(SOURCES) 111 | $(CC) $(CFLAGS) -Ae -o qstat $(SRC) $(LDFLAGS) $(LDLIBS) 112 | 113 | win32: windows 114 | 115 | .c.obj: 116 | $(CC) $(CFLAGS) -c $< 117 | 118 | # windows dynamic rebuild but doesn't handle a QSTAT_VERSION change. 119 | # use the clean target to force a rebuild. 120 | qstat.exe: version.h $(OBJ) 121 | $(LINK) $(LFLAGS) $(OBJ) $(WINDOWS_LIBS) /out:$@ 122 | 123 | # windows release forced 124 | windows: $(SOURCES) 125 | $(CC) $(CFLAGS) $(SRC) /Feqstat.exe $(WINDOWS_LIBS) 126 | 127 | # windows debug forced 128 | windows_debug: $(SOURCES) 129 | $(CC) $(CFLAGS) /Zi $(SRC) /Feqstat.exe $(WINDOWS_LIBS) /link /fixed:no /incremental:no 130 | 131 | clean: 132 | -rm -f qstat core qstat.exe *.pdb .compiler_flags .version version.h $(O) $(OBJ) 133 | -------------------------------------------------------------------------------- /bfbc2.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Battlefield Bad Company 2 query protocol 6 | * Copyright 2009 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | #include 21 | #include 22 | #include 23 | 24 | #include "debug.h" 25 | #include "qstat.h" 26 | #include "packet_manip.h" 27 | 28 | query_status_t 29 | send_bfbc2_request_packet(struct qserver *server) 30 | { 31 | char buf[50]; 32 | int size = 0; 33 | 34 | switch (server->challenge) { 35 | case 0: 36 | // Initial connect send serverInfo 37 | size = 27; 38 | memcpy(buf, "\x00\x00\x00\x00\x1b\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x00\x00serverInfo\x00", size); 39 | break; 40 | 41 | case 1: 42 | // All Done send quit 43 | size = 21; 44 | memcpy(buf, "\x01\x00\x00\x00\x15\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00quit\x00", size); 45 | break; 46 | 47 | case 2: 48 | return (DONE_FORCE); 49 | } 50 | debug(3, "send_bfbc2_request_packet: state = %ld", server->challenge); 51 | 52 | return (send_packet(server, buf, size)); 53 | } 54 | 55 | 56 | query_status_t 57 | deal_with_bfbc2_packet(struct qserver *server, char *rawpkt, int pktlen) 58 | { 59 | char *s, *end, *crlf; 60 | int words, word = 0; 61 | 62 | debug(2, "processing..."); 63 | 64 | if (17 > pktlen) { 65 | // Invalid packet 66 | return (REQ_ERROR); 67 | } 68 | 69 | rawpkt[pktlen - 1] = '\0'; 70 | end = &rawpkt[pktlen - 1]; 71 | s = rawpkt; 72 | 73 | server->ping_total = time_delta(&packet_recv_time, &server->packet_time1); 74 | server->n_requests++; 75 | 76 | // Header Sequence 77 | s += 4; 78 | 79 | // Packet Size 80 | s += 4; 81 | 82 | // Num Words 83 | words = *(int *)s; 84 | s += 4; 85 | 86 | // Words 87 | while (words > 0 && s + 5 < end) { 88 | // Size 89 | int ws = *(int *)s; 90 | s += 4; 91 | 92 | // Content 93 | debug(6, "word: %s\n", s); 94 | switch (word) { 95 | case 0: 96 | // Status 97 | break; 98 | 99 | case 1: 100 | // Server Name 101 | // prevent CR & LF in the server name 102 | crlf = strchr(s, '\015'); 103 | if (NULL != crlf) { 104 | *crlf = '\0'; 105 | } 106 | crlf = strchr(s, '\012'); 107 | if (NULL != crlf) { 108 | *crlf = '\0'; 109 | } 110 | server->server_name = strdup(s); 111 | break; 112 | 113 | case 2: 114 | // Player Count 115 | server->num_players = atoi(s); 116 | break; 117 | 118 | case 3: 119 | // Max Players 120 | server->max_players = atoi(s); 121 | break; 122 | 123 | case 4: 124 | // Game Mode 125 | add_rule(server, "gametype", s, NO_FLAGS); 126 | break; 127 | 128 | case 5: 129 | // Map 130 | server->map_name = strdup(s); 131 | break; 132 | } 133 | word++; 134 | 135 | s += ws + 1; 136 | 137 | words--; 138 | } 139 | 140 | server->challenge++; 141 | gettimeofday(&server->packet_time1, NULL); 142 | 143 | if (1 == server->challenge) { 144 | send_bfbc2_request_packet(server); 145 | return (INPROGRESS); 146 | } 147 | 148 | return (DONE_FORCE); 149 | } 150 | -------------------------------------------------------------------------------- /md5.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 2. Altered source versions must be plainly marked as such, and must not be 17 | * misrepresented as being the original software. 18 | * 3. This notice may not be removed or altered from any source distribution. 19 | * 20 | * L. Peter Deutsch 21 | * ghost@aladdin.com 22 | * 23 | */ 24 | /* $Id$ */ 25 | 26 | /* 27 | * Independent implementation of MD5 (RFC 1321). 28 | * 29 | * This code implements the MD5 Algorithm defined in RFC 1321, whose 30 | * text is available at 31 | * http://www.ietf.org/rfc/rfc1321.txt 32 | * The code is derived from the text of the RFC, including the test suite 33 | * (section A.5) but excluding the rest of Appendix A. It does not include 34 | * any code or documentation that is identified in the RFC as being 35 | * copyrighted. 36 | * 37 | * The original and principal author of md5.h is L. Peter Deutsch 38 | * . Other authors are noted in the change history 39 | * that follows (in reverse chronological order): 40 | * 41 | * 2002-04-13 lpd Removed support for non-ANSI compilers; removed 42 | * references to Ghostscript; clarified derivation from RFC 1321; 43 | * now handles byte order either statically or dynamically. 44 | * 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. 45 | * 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); 46 | * added conditionalization for C++ compilation from Martin 47 | * Purschke . 48 | * 1999-05-03 lpd Original version. 49 | */ 50 | 51 | #ifndef md5_INCLUDED 52 | #define md5_INCLUDED 53 | 54 | /* 55 | * This package supports both compile-time and run-time determination of CPU 56 | * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be 57 | * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is 58 | * defined as non-zero, the code will be compiled to run only on big-endian 59 | * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to 60 | * run on either big- or little-endian CPUs, but will run slightly less 61 | * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. 62 | */ 63 | 64 | typedef unsigned char md5_byte_t; /* 8-bit byte */ 65 | typedef unsigned int md5_word_t; /* 32-bit word */ 66 | 67 | /* Define the state of the MD5 Algorithm. */ 68 | typedef struct md5_state_s { 69 | md5_word_t count[2]; /* message length in bits, lsw first */ 70 | md5_word_t abcd[4]; /* digest buffer */ 71 | md5_byte_t buf[64]; /* accumulate block */ 72 | } md5_state_t; 73 | 74 | #ifdef __cplusplus 75 | extern "C" 76 | { 77 | #endif 78 | 79 | /* Initialize the algorithm. */ 80 | void md5_init(md5_state_t *pms); 81 | 82 | /* Append a string to the message. */ 83 | void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes); 84 | 85 | /* Finish the message and return the digest. */ 86 | void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); 87 | 88 | /* Return an hex md5 of the given string */ 89 | char *md5_hex(const char *bytes, int nbytes); 90 | 91 | #ifdef __cplusplus 92 | } /* end extern "C" */ 93 | #endif 94 | 95 | #endif /* md5_INCLUDED */ 96 | -------------------------------------------------------------------------------- /qserver.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * qserver functions 6 | * Copyright 2004 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | 11 | #include "qstat.h" 12 | #include "qserver.h" 13 | #include "debug.h" 14 | 15 | #ifndef _WIN32 16 | #include 17 | #include 18 | #endif 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | // TODO: get rid of this and use send_packet instead, remove n_requests hack from a2s 26 | query_status_t 27 | qserver_send_initial(struct qserver *server, const char *data, size_t len) 28 | { 29 | query_status_t status; 30 | 31 | status = send_packet_raw(server, data, len); 32 | 33 | if ((server->retry1 == n_retries) || server->flags & FLAG_BROADCAST) { 34 | gettimeofday(&server->packet_time1, NULL); 35 | } else { 36 | server->n_retries++; 37 | } 38 | 39 | server->retry1--; 40 | server->n_packets++; 41 | 42 | return (status); 43 | } 44 | 45 | query_status_t 46 | qserver_send(struct qserver *server, const char *data, size_t len) 47 | { 48 | query_status_t status; 49 | 50 | status = send_packet_raw(server, data, len); 51 | 52 | server->retry1 = n_retries - 1; 53 | gettimeofday(&server->packet_time1, NULL); 54 | server->n_requests++; 55 | server->n_packets++; 56 | 57 | return (status); 58 | } 59 | 60 | int 61 | send_broadcast(struct qserver *server, const char *pkt, size_t pktlen) 62 | { 63 | struct sockaddr_in addr; 64 | 65 | addr.sin_family = AF_INET; 66 | if (no_port_offset || server->flags & TF_NO_PORT_OFFSET) { 67 | addr.sin_port = htons(server->port); 68 | } else { 69 | addr.sin_port = htons((unsigned short)(server->port + server->type->port_offset)); 70 | } 71 | addr.sin_addr.s_addr = server->ipaddr; 72 | memset(&(addr.sin_zero), 0, sizeof(addr.sin_zero)); 73 | 74 | return (sendto(server->fd, (const char *)pkt, pktlen, 0, (struct sockaddr *)&addr, sizeof(addr))); 75 | } 76 | 77 | int 78 | register_send(struct qserver *server) 79 | { 80 | if ((server->retry1 == n_retries) || server->flags & FLAG_BROADCAST) { 81 | server->n_requests++; 82 | } else { 83 | server->n_retries++; 84 | } 85 | 86 | // New request so reset the sent time. This ensures 87 | // that we record an accurate ping time even on retry 88 | gettimeofday(&server->packet_time1, NULL); 89 | server->retry1--; 90 | server->n_packets++; 91 | 92 | return (INPROGRESS); 93 | } 94 | 95 | query_status_t 96 | send_packet_raw(struct qserver *server, const char *data, size_t len) 97 | { 98 | debug(2, "[%s] send", server->type->type_prefix); 99 | if (4 <= get_debug_level()) { 100 | output_packet(server, data, len, 1); 101 | } 102 | 103 | if (data) { 104 | int ret; 105 | if (server->flags & FLAG_BROADCAST) { 106 | ret = send_broadcast(server, data, len); 107 | } else { 108 | ret = send(server->fd, data, len, 0); 109 | } 110 | 111 | if (ret == SOCKET_ERROR) { 112 | return (send_error(server, ret)); 113 | } 114 | } 115 | 116 | return (INPROGRESS); 117 | } 118 | 119 | query_status_t 120 | send_packet(struct qserver *server, const char *data, size_t len) 121 | { 122 | query_status_t rc; 123 | 124 | rc = send_packet_raw(server, data, len); 125 | if (rc == SYS_ERROR) { 126 | return (rc); 127 | } 128 | 129 | return (register_send(server)); 130 | } 131 | 132 | query_status_t 133 | send_error(struct qserver *server, int rc) 134 | { 135 | unsigned int ipaddr = ntohl(server->ipaddr); 136 | const char *errstr = strerror(errno); 137 | 138 | fprintf(stderr, "Error on %d.%d.%d.%d: %s, skipping ...\n", 139 | (ipaddr >> 24) & 0xff, 140 | (ipaddr >> 16) & 0xff, 141 | (ipaddr >> 8) & 0xff, 142 | ipaddr & 0xff, 143 | errstr 144 | ); 145 | return (SYS_ERROR); 146 | } 147 | -------------------------------------------------------------------------------- /ts2.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Teamspeak 2 query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #endif 16 | #include 17 | #include 18 | #include 19 | 20 | #include "debug.h" 21 | #include "qstat.h" 22 | #include "packet_manip.h" 23 | 24 | query_status_t 25 | send_ts2_request_packet(struct qserver *server) 26 | { 27 | char buf[256]; 28 | 29 | int serverport = get_param_i_value(server, "port", 0); 30 | 31 | change_server_port(server, serverport, 1); 32 | 33 | if (get_player_info) { 34 | server->flags |= TF_PLAYER_QUERY | TF_RULES_QUERY; 35 | sprintf(buf, "si %d\npl %d\nquit\n", serverport, serverport); 36 | server->saved_data.pkt_index = 2; 37 | } else { 38 | server->flags |= TF_STATUS_QUERY; 39 | sprintf(buf, "si %d\nquit\n", serverport); 40 | server->saved_data.pkt_index = 1; 41 | } 42 | 43 | return (send_packet(server, buf, strlen(buf))); 44 | } 45 | 46 | 47 | query_status_t 48 | deal_with_ts2_packet(struct qserver *server, char *rawpkt, int pktlen) 49 | { 50 | char *s; 51 | int ping, connect_time, mode = 0; 52 | char name[256]; 53 | 54 | debug(2, "processing..."); 55 | 56 | server->n_servers++; 57 | server->n_requests++; 58 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 59 | 60 | if (0 == pktlen) { 61 | // Invalid password 62 | return (REQ_ERROR); 63 | } 64 | 65 | rawpkt[pktlen] = '\0'; 66 | 67 | s = rawpkt; 68 | s = strtok(rawpkt, "\015\012"); 69 | 70 | while (NULL != s) { 71 | if (0 == mode) { 72 | // Rules 73 | char *key = s; 74 | char *value = strchr(key, '='); 75 | if (NULL != value) { 76 | // Server Rule 77 | *value = '\0'; 78 | value++; 79 | if (0 == strcmp("server_name", key)) { 80 | server->server_name = strdup(value); 81 | } else if (0 == strcmp("server_udpport", key)) { 82 | change_server_port(server, atoi(value), 0); 83 | add_rule(server, key, value, NO_FLAGS); 84 | } else if (0 == strcmp("server_maxusers", key)) { 85 | server->max_players = atoi(value); 86 | } else if (0 == strcmp("server_currentusers", key)) { 87 | server->num_players = atoi(value); 88 | } else { 89 | add_rule(server, key, value, NO_FLAGS); 90 | } 91 | } else if (0 == strcmp("OK", s)) { 92 | // end of rules request 93 | server->saved_data.pkt_index--; 94 | mode++; 95 | } else if (0 == strcmp("[TS]", s)) { 96 | // nothing to do 97 | } else if (0 == strcmp("ERROR, invalid id", s)) { 98 | // bad server 99 | server->server_name = DOWN; 100 | server->saved_data.pkt_index = 0; 101 | } 102 | } else if (1 == mode) { 103 | // Player info 104 | if (3 == sscanf(s, "%*d %*d %*d %*d %*d %*d %*d %d %d %*d %*d %*d %*d \"0.0.0.0\" \"%255[^\"]", &ping, &connect_time, name)) { 105 | // Player info 106 | struct player *player = add_player(server, server->n_player_info); 107 | if (NULL != player) { 108 | player->name = strdup(name); 109 | player->ping = ping; 110 | player->connect_time = connect_time; 111 | } 112 | } else if (0 == strcmp("OK", s)) { 113 | // end of rules request 114 | server->saved_data.pkt_index--; 115 | mode++; 116 | } else if (0 == strcmp("[TS]", s)) { 117 | // nothing to do 118 | } else if (0 == strcmp("ERROR, invalid id", s)) { 119 | // bad server 120 | server->server_name = DOWN; 121 | server->saved_data.pkt_index = 0; 122 | } 123 | } 124 | s = strtok(NULL, "\015\012"); 125 | } 126 | 127 | gettimeofday(&server->packet_time1, NULL); 128 | 129 | if (0 == server->saved_data.pkt_index) { 130 | server->map_name = strdup("N/A"); 131 | return (DONE_FORCE); 132 | } 133 | 134 | return (INPROGRESS); 135 | } 136 | -------------------------------------------------------------------------------- /template/ghostreconTs.html: -------------------------------------------------------------------------------- 1 |
Server Name 25 | Players 26 | Map 27 | Game 28 | Address 29 |
2 | 3 | 9 | 10 | 16 | 18 | 24 | 25 | 31 | 33 | 39 | 40 | 46 | 48 | 54 | 55 | 61 | 63 | 69 | 70 | 76 | 78 | 84 | 85 | 91 | 93 | 99 | 100 | 106 |
Server Name 4 | Players 5 | Map 6 | Mission 7 |   8 |
$SERVERNAME 11 | $PLAYERS/$MAXPLAYERS 12 | $MAP 13 | $(RULE:mission) 14 |   15 |
17 |
Game Mode 19 | Mission Type 20 | Dedicated Server 21 | Ip Addr 22 |   23 |
$(RULE:gamemode) 26 | $(RULE:missiontype) 27 | $(RULE:dedicated) 28 | $IPADDR:$PORT 29 |   30 |
32 |
Status 34 | Time Limit 35 | Time Played 36 | Remaining Time 37 |   38 |
$(RULE:status) 41 | $(RULE:gametime) 42 | $(RULE:timeplayed) 43 | $(RULE:remainingtime) 44 |   45 |
47 |
Version 49 | Spawn Type 50 | Spawn Count 51 | Restrictions 52 |   53 |
$(RULE:version) 56 | $(RULE:spawntype) 57 | $(RULE:spawncount) 58 | $(RULE:restrict) 59 |   60 |
62 |
Password 64 | Threat Indicator 65 | Patch Level 66 | Observing 67 |   68 |
$(RULE:password) 71 | $(RULE:ti) 72 | $(RULE:patch) 73 | $(RULE:allowobservers) 74 |   75 |
77 |
Start Timer 79 | Start Time Value 80 | Time untill Start 81 | Debrief Time 82 |   83 |
$(RULE:usestarttime) 86 | $(RULE:starttimeset) 87 | $(RULE:startwait) 88 | $(RULE:debrieftime) 89 |   90 |
92 |
Respawn Minimum 94 | Respawn Maximum 95 | Respawn Safe 96 | IFF Mode 97 |   98 |
$(RULE:respawnmin) 101 | $(RULE:respawnmax) 102 | $(RULE:respawnsafe) 103 | $(RULE:iff) 104 |   105 |
107 |
108 | 109 | 110 | 113 | 114 | 117 | 118 | 121 | 122 | 125 |
MOTD 111 |   112 |
$(RULE:motd) 115 |   116 |
Mods 119 |   120 |
$GAME 123 |   124 |
126 |
127 | $(IF:PLAYERS)$(IF:FLAG(-P)) 128 | 129 | 130 | 131 | 132 | 133 | 134 | $PLAYERTEMPLATE 135 |
Player NameTeamHealth 
136 | $ENDIF$ENDIF 137 |

138 | -------------------------------------------------------------------------------- /debug.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * debug helper functions 6 | * Copyright 2004 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | 11 | #include 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #else 17 | #include 18 | #endif 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #define QSTAT_DEBUG_C 28 | #include "debug.h" 29 | 30 | #ifdef ENABLE_DUMP 31 | #ifndef _WIN32 32 | #include 33 | #else 34 | #include 35 | #endif 36 | #endif 37 | 38 | #ifdef DEBUG 39 | 40 | void 41 | _debug(const char *file, int line, const char *function, int level, const char *fmt, ...) 42 | { 43 | va_list ap; 44 | char buf[9]; 45 | time_t now = time(NULL); 46 | 47 | strftime(buf, 9, "%H:%M:%S", localtime(&now)); 48 | 49 | fprintf(stderr, "debug(%d) %s %s:%d %s() - ", level, buf, file, line, function); 50 | 51 | va_start(ap, fmt); 52 | vfprintf(stderr, fmt, ap); 53 | va_end(ap); 54 | if ('\n' != fmt[strlen(fmt) - 1]) { 55 | fputs("\n", stderr); 56 | } 57 | } 58 | 59 | 60 | #endif 61 | 62 | static int debug_level = 0; 63 | 64 | void 65 | set_debug_level(int level) 66 | { 67 | debug_level = level; 68 | } 69 | 70 | 71 | int 72 | get_debug_level(void) 73 | { 74 | return (debug_level); 75 | } 76 | 77 | 78 | void 79 | malformed_packet(const struct qserver *server, const char *fmt, ...) 80 | { 81 | va_list ap; 82 | 83 | if (!show_errors) { 84 | return; 85 | } 86 | 87 | fputs("malformed packet", stderr); 88 | 89 | if (server) { 90 | fprintf(stderr, " from %d.%d.%d.%d:%hu", 91 | server->ipaddr & 0xff, 92 | (server->ipaddr >> 8) & 0xff, 93 | (server->ipaddr >> 16) & 0xff, 94 | (server->ipaddr >> 24) & 0xff, 95 | server->port); 96 | } 97 | fputs(": ", stderr); 98 | 99 | va_start(ap, fmt); 100 | vfprintf(stderr, fmt, ap); 101 | va_end(ap); 102 | 103 | if (*fmt && (fmt[strlen(fmt) - 1] != '\n')) { 104 | fputc('\n', stderr); 105 | } 106 | } 107 | 108 | 109 | #ifdef ENABLE_DUMP 110 | static unsigned count = 0; 111 | static void 112 | _dump_packet(const char *tag, const char *buf, int buflen) 113 | { 114 | char fn[PATH_MAX] = { 0 }; 115 | int fd; 116 | 117 | sprintf(fn, "%03u_%s.pkt", count++, tag); 118 | fprintf(stderr, "dumping to %s\n", fn); 119 | fd = open(fn, O_WRONLY | O_CREAT | O_EXCL, 0644); 120 | if (fd == -1) { 121 | perror("open"); 122 | return; 123 | } 124 | if (write(fd, buf, buflen) == -1) { 125 | perror("write"); 126 | } 127 | close(fd); 128 | } 129 | 130 | int do_dump; 131 | ssize_t 132 | send_dump(int s, const void *buf, size_t len, int flags) 133 | { 134 | if (do_dump) { 135 | _dump_packet("send", buf, len); 136 | } 137 | return (send(s, buf, len, flags)); 138 | } 139 | 140 | 141 | #endif 142 | 143 | void 144 | dump_packet(const char *buf, int buflen) 145 | { 146 | #ifdef ENABLE_DUMP 147 | _dump_packet("recv", buf, buflen); 148 | #endif 149 | } 150 | 151 | 152 | void 153 | print_packet(struct qserver *server, const char *buf, int buflen) 154 | { 155 | output_packet(server, buf, buflen, 0); 156 | } 157 | 158 | 159 | void 160 | output_packet(struct qserver *server, const char *buf, int buflen, int to) 161 | { 162 | static char *hex = "0123456789abcdef"; 163 | unsigned char *p = (unsigned char *)buf; 164 | int i, h, a, b, astart, offset = 0; 165 | char line[256]; 166 | 167 | if (server != NULL) { 168 | fprintf(stderr, "%s %s len %d\n", (to) ? "TO" : "FROM", server->arg, buflen); 169 | } 170 | 171 | for (i = buflen; i; offset += 16) { 172 | memset(line, ' ', 256); 173 | h = 0; 174 | h += sprintf(line, "%5d:", offset); 175 | a = astart = h + 16 * 2 + 16 / 4 + 2; 176 | for (b = 16; b && i; b--, i--, p++) { 177 | if ((b & 3) == 0) { 178 | line[h++] = ' '; 179 | } 180 | line[h++] = hex[*p >> 4]; 181 | line[h++] = hex[*p & 0xf]; 182 | if (isprint(*p)) { 183 | line[a++] = *p; 184 | } else { 185 | line[a++] = '.'; 186 | } 187 | if ((a - astart) == 8) { 188 | line[a++] = ' '; 189 | } 190 | } 191 | line[a] = '\0'; 192 | fputs(line, stderr); 193 | fputs("\n", stderr); 194 | } 195 | fputs("\n", stderr); 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qstat 2 | QStat is a command-line program that displays information about Internet game servers. The servers are either down, non-responsive, or running a game. For servers running a game, the server name, map name, current number of players, and response time are displayed. Server rules and player information may also be displayed. 3 | 4 | ## Supported Games 5 | Games supported include Quake, QuakeWorld, Hexen II, Quake II, HexenWorld, Unreal, Half-Life, Sin, Shogo, Tribes, Tribes 2, Quake III: Arena, BFRIS, Kingpin, and Heretic II, Unreal Tournament, Soldier of Fortune, Rogue Spear, Redline, Turok II, Blood 2, Descent 3, Drakan, KISS, Nerf Arena Blast, Rally Master, Terminous, Wheel of Time, and Daikatana and many more. Note for Tribes 2: QStat only supports Tribes 2 builds numbered 22075 or higher. Note for Ghost Recon QStat only supports GhostRecon patch 1.2, 1.3, 1.4, Desert Siege, and Island Thunder. 6 | 7 | This list is currently outdated, for a full list see the output from qstat itself. 8 | 9 | The different server types can be queried simultaneously. If qStat detects that this is being done, the output is keyed by the type of server being displayed. See Display Options. 10 | 11 | The game server may be specified as an IP address or a hostname. Servers can be listed on the command-line or, with the use of the -f option, a text file. 12 | 13 | ## Display Options 14 | One line will be displayed for each server queried. The first component of the line will be the server's address as given on the command-line or the file. This can be used as a key to match input addresses to server status. Server rules and player information are displayed under the server info, indented by one tab stop. 15 | 16 | qstat supports three additional display modes: raw, templates, and XML. In raw mode, the server information is displayed using simple delimiters and no formatting. This mode is good for programs that parse and reformat QStat's output. The template mode uses text files to layout the server information within existing text. This is ideal for generating web pages. The XML mode outputs server information wrapped in simple XML tags. The raw mode is enabled using the -raw option, template output is enabled using -Ts, and XML output is enabled with -xml. 17 | 18 | ## Game Options 19 | These options select which servers to query and what game type they are running. Servers are specified by IP address (for example: 199.2.18.4) or hostname. Servers can be listed on the command-line or in a file (see option -f.) The game type of a server can be specified with its address, or a default game type can be set for all addresses that don't have a game type. 20 | 21 | The following table shows the command-line option and type strings for the supported game types. The type string is used with the -default option and in files with the -f option. 22 | 23 | ## Configuration files 24 | The games supported by qstat can be customized with configuration files. The query parameters of built-in game types can be modified and new games can be defined. 25 | For built-in game types, certain parameters can be modified. The parameters are limited to the master server protocol and master server query string. 26 | 27 | New game types can be defined as a variation on an existing game type. Most new games use a Quake 3 or Gamespy/Unreal based network engine. These games can already be queried using -q3s or -gps, but they don't have game specific details such as the correct default port, the game name, and the correct "game" or "mod" server rule. And, mostly importantly, they don't get their own game type string (e.g. q3s, rws, t2s). All of these details can be specified in the QStat config file. 28 | 29 | qstat comes with a default configuration file called 'qstat.cfg'. If this file is found in the directory where qstat is run, the file will be loaded. Configuration files can also be specified with the QSTAT_CONFIG environment variable and the -cfg command-line option. See Appendix B for a description of the configuration file format. 30 | 31 | ## Migration to Github 32 | I've been the only active maintainer for the old [qstat sourceforge repo](http://sourceforge.net/projects/qstat/) for a while now. Unfortunately I don't have full admin their and have been unable to contact the original qstat project creator [Steve Jankowski](https://sourceforge.net/u/stevejankowski/profile/) for some time, so I've decided to move the project to github. 33 | 34 | Hence for all intensive purposes https://github.com/multiplay/qstat is the new official home of the qstat repro. 35 | -------------------------------------------------------------------------------- /utils.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Utility Functions 3 | * 4 | */ 5 | 6 | #include "utils.h" 7 | 8 | #ifndef _WIN32 9 | #include 10 | #include 11 | #endif 12 | 13 | #if !HAVE_STRNSTR 14 | 15 | /* 16 | * strnstr - 17 | * Copyright (c) 2001 Mike Barcroft 18 | * Copyright (c) 1990, 1993 19 | * The Regents of the University of California. All rights reserved. 20 | * 21 | * This code is derived from software contributed to Berkeley by 22 | * Chris Torek. 23 | * 24 | * Redistribution and use in source and binary forms, with or without 25 | * modification, are permitted provided that the following conditions 26 | * are met: 27 | * 1. Redistributions of source code must retain the above copyright 28 | * notice, this list of conditions and the following disclaimer. 29 | * 2. Redistributions in binary form must reproduce the above copyright 30 | * notice, this list of conditions and the following disclaimer in the 31 | * documentation and/or other materials provided with the distribution. 32 | * 3. All advertising materials mentioning features or use of this software 33 | * must display the following acknowledgement: 34 | * This product includes software developed by the University of 35 | * California, Berkeley and its contributors. 36 | * 4. Neither the name of the University nor the names of its contributors 37 | * may be used to endorse or promote products derived from this software 38 | * without specific prior written permission. 39 | * 40 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 41 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 42 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 43 | * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 44 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 45 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 46 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 47 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 48 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 49 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 50 | * SUCH DAMAGE. 51 | */ 52 | 53 | #include 54 | 55 | char * 56 | qstat_strnstr(const char *s, const char *find, size_t slen) 57 | { 58 | char c, sc; 59 | size_t len; 60 | 61 | if ((c = *find++) != '\0') { 62 | len = strlen(find); 63 | do { 64 | do { 65 | if ((slen-- < 1) || ((sc = *s++) == '\0')) { 66 | return (NULL); 67 | } 68 | } while (sc != c); 69 | if (len > slen) { 70 | return (NULL); 71 | } 72 | } while (strncmp(s, find, len) != 0); 73 | s--; 74 | } 75 | return ((char *)s); 76 | } 77 | 78 | 79 | #endif /* !HAVE_STRNSTR */ 80 | 81 | #if !HAVE_STRNDUP 82 | 83 | #include 84 | #include 85 | 86 | char * 87 | strndup(const char *string, size_t len) 88 | { 89 | char *result; 90 | 91 | result = (char *)malloc(len + 1); 92 | memcpy(result, string, len); 93 | result[len] = '\0'; 94 | 95 | return (result); 96 | } 97 | 98 | 99 | #endif /* !HAVE_STRNDUP */ 100 | 101 | #if !HAVE_ERR_H 102 | 103 | #include 104 | #include 105 | #include 106 | 107 | void 108 | err(int eval, const char *fmt, ...) 109 | { 110 | va_list list; 111 | 112 | va_start(list, fmt); 113 | warn(fmt, list); 114 | va_end(list); 115 | 116 | exit(eval); 117 | } 118 | 119 | 120 | void 121 | warn(const char *fmt, ...) 122 | { 123 | va_list list; 124 | 125 | va_start(list, fmt); 126 | if (fmt) { 127 | fprintf(stderr, fmt, list); 128 | } 129 | fprintf(stderr, "%s\n", strerror(errno)); 130 | va_end(list); 131 | } 132 | 133 | 134 | #endif /* HAVE_ERR_H */ 135 | 136 | #include 137 | 138 | // NOTE: replace must be smaller or equal in size to find 139 | char * 140 | str_replace(char *source, char *find, char *replace) 141 | { 142 | char *s = strstr(source, find); 143 | int rlen = strlen(replace); 144 | int flen = strlen(find); 145 | 146 | if (rlen > flen) { 147 | err(EX_SOFTWARE, "str_replace: replace is larger than find"); 148 | } 149 | 150 | while (NULL != s) { 151 | strncpy(s, replace, rlen); // -Wstringop-truncation warning here is a false positive. 152 | if (rlen < flen) { 153 | strcpy(s + rlen, s + flen); 154 | } 155 | s += rlen; 156 | s = strstr(s, find); 157 | } 158 | 159 | return (source); 160 | } 161 | -------------------------------------------------------------------------------- /wic.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * World in Conflict Protocol 6 | * Copyright 2007 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #endif 16 | #include 17 | #include 18 | #include 19 | 20 | #include "debug.h" 21 | #include "qstat.h" 22 | #include "packet_manip.h" 23 | 24 | query_status_t 25 | send_wic_request_packet(struct qserver *server) 26 | { 27 | char buf[256]; 28 | 29 | int serverport = get_param_i_value(server, "port", 0); 30 | char *password = get_param_value(server, "password", "N/A"); 31 | 32 | change_server_port(server, serverport, 1); 33 | 34 | if (get_player_info) { 35 | server->flags |= TF_PLAYER_QUERY | TF_RULES_QUERY; 36 | sprintf(buf, "%s\x0d\x0a/listsettings\x0d\x0a/listplayers\x0d\x0a/exit\x0d\x0a", password); 37 | server->saved_data.pkt_index = 2; 38 | } else { 39 | server->flags |= TF_STATUS_QUERY; 40 | sprintf(buf, "%s\x0d\x0a/listsettings\x0d\x0a/exit\x0d\x0a", password); 41 | server->saved_data.pkt_index = 1; 42 | } 43 | 44 | return (send_packet(server, buf, strlen(buf))); 45 | } 46 | 47 | 48 | query_status_t 49 | deal_with_wic_packet(struct qserver *server, char *rawpkt, int pktlen) 50 | { 51 | char *s, *end, *team = NULL; 52 | int mode = server->n_servers, slot, score; 53 | char name[256], role[256]; 54 | 55 | debug(2, "processing n_requests %d, retry1 %d, n_retries %d, delta %d", server->n_requests, server->retry1, n_retries, time_delta(&packet_recv_time, &server->packet_time1)); 56 | 57 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 58 | server->n_requests++; 59 | 60 | if (0 == pktlen) { 61 | // Invalid password 62 | return (REQ_ERROR); 63 | } 64 | 65 | gettimeofday(&server->packet_time1, NULL); 66 | 67 | rawpkt[pktlen] = '\0'; 68 | end = &rawpkt[pktlen]; 69 | 70 | s = rawpkt; 71 | 72 | while (NULL != s) { 73 | int len = strlen(s); 74 | *(s + len - 2) = '\0'; // strip off \x0D\x0A 75 | debug(2, "Line[%d]: %s", mode, s); 76 | 77 | if (0 == mode) { 78 | // Settings 79 | // TODO: make parse safe 80 | if (0 == strncmp(s, "Settings: ", 9)) { 81 | // Server Rule 82 | char *key = s + 10; 83 | char *value = strchr(key, ' '); 84 | *value = '\0'; 85 | value++; 86 | debug(2, "key: '%s' = '%s'", key, value); 87 | if (0 == strcmp("myGameName", key)) { 88 | server->server_name = strdup(value); 89 | } else if (0 == strcmp("myMapFilename", key)) { 90 | server->map_name = strdup(value); 91 | } else if (0 == strcmp("myMaxPlayers", key)) { 92 | server->max_players = atoi(value); 93 | } else if (0 == strcmp("myCurrentNumberOfPlayers", key)) { 94 | server->num_players = atoi(value); 95 | } else { 96 | add_rule(server, key, value, NO_FLAGS); 97 | } 98 | } else if (0 == strcmp("Listing players", s)) { 99 | // end of rules request 100 | server->saved_data.pkt_index--; 101 | mode++; 102 | } else if (0 == strcmp("Exit confirmed.", s)) { 103 | server->n_servers = mode; 104 | return (DONE_FORCE); 105 | } 106 | } else if (1 == mode) { 107 | // Player info 108 | if (0 == strncmp(s, "Team: ", 6)) { 109 | team = s + 6; 110 | debug(2, "Team: %s", team); 111 | } else if (4 == sscanf(s, "Slot: %d Role: %s Score: %d Name: %255[^\x0d\x0a]", &slot, role, &score, name)) { 112 | // Player info 113 | struct player *player = add_player(server, server->n_player_info); 114 | if (NULL != player) { 115 | player->flags |= PLAYER_FLAG_DO_NOT_FREE_TEAM; 116 | player->name = strdup(name); 117 | player->score = score; 118 | player->team_name = team; 119 | player->tribe_tag = strdup(role); 120 | // Indicate if its a bot 121 | player->type_flag = (0 == strcmp(name, "Computer: Balanced")) ? 1 : 0; 122 | } 123 | debug(2, "player %d, role %s, score %d, name %s", slot, role, score, name); 124 | } else if (3 == sscanf(s, "Slot: %d Role: Score: %d Name: %255[^\x0d\x0a]", &slot, &score, name)) { 125 | // Player info 126 | struct player *player = add_player(server, server->n_player_info); 127 | if (NULL != player) { 128 | player->flags |= PLAYER_FLAG_DO_NOT_FREE_TEAM; 129 | player->name = strdup(name); 130 | player->score = score; 131 | player->team_name = team; 132 | // Indicate if its a bot 133 | player->type_flag = (0 == strcmp(name, "Computer: Balanced")) ? 1 : 0; 134 | } 135 | debug(2, "player %d, score %d, name %s", slot, score, name); 136 | } else if (0 == strcmp("Exit confirmed.", s)) { 137 | server->n_servers = mode; 138 | return (DONE_FORCE); 139 | } 140 | } 141 | 142 | s += len; 143 | if (s + 1 < end) { 144 | s++;// next line 145 | } else { 146 | s = NULL; 147 | } 148 | } 149 | 150 | server->n_servers = mode; 151 | 152 | if (0 == server->saved_data.pkt_index) { 153 | server->map_name = strdup("N/A"); 154 | return (DONE_FORCE); 155 | } 156 | 157 | return (INPROGRESS); 158 | } 159 | -------------------------------------------------------------------------------- /terraria.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Terraria / TShock query protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #define strtok_ret strtok_r 18 | #else 19 | #include 20 | #define strtok_ret strtok_s 21 | #endif 22 | #include 23 | #include 24 | #include 25 | 26 | #include "debug.h" 27 | #include "qstat.h" 28 | #include "packet_manip.h" 29 | 30 | query_status_t 31 | send_terraria_request_packet(struct qserver *server) 32 | { 33 | return (send_packet(server, server->type->status_packet, server->type->status_len)); 34 | } 35 | 36 | 37 | query_status_t 38 | deal_with_terraria_packet(struct qserver *server, char *rawpkt, int pktlen) 39 | { 40 | char *s, *key, *val, *sep, *linep, *varp; 41 | int complete_response = 0; 42 | char last_char; 43 | unsigned short port = 0; 44 | 45 | debug(2, "processing..."); 46 | 47 | if (0 == pktlen) { 48 | // Disconnect? 49 | return (REQ_ERROR); 50 | } 51 | 52 | complete_response = (0 == strncmp("HTTP/1.1 200", rawpkt, 12) && '}' == rawpkt[pktlen - 1]) ? 1 : 0; 53 | last_char = rawpkt[pktlen - 1]; 54 | rawpkt[pktlen - 1] = '\0'; 55 | 56 | debug(3, "packet: combined = %d, complete = %d", server->combined, complete_response); 57 | if (!complete_response) { 58 | if (!server->combined) { 59 | // response fragment recieved 60 | int pkt_id; 61 | int pkt_max; 62 | server->retry1 = n_retries; 63 | if (0 == server->n_requests) { 64 | server->ping_total = time_delta(&packet_recv_time, &server->packet_time1); 65 | server->n_requests++; 66 | } 67 | 68 | // We're expecting more to come 69 | debug(5, "fragment recieved..."); 70 | pkt_id = packet_count(server); 71 | pkt_max = pkt_id + 1; 72 | rawpkt[pktlen - 1] = last_char; // restore the last character 73 | if (!add_packet(server, 0, pkt_id, pkt_max, pktlen, rawpkt, 1)) { 74 | // fatal error e.g. out of memory 75 | return (MEM_ERROR); 76 | } 77 | 78 | if (0 == pkt_id) { 79 | return (INPROGRESS); 80 | } 81 | 82 | // combine_packets will call us recursively 83 | return (combine_packets(server)); 84 | } 85 | 86 | // recursive call which is still incomplete 87 | return (INPROGRESS); 88 | } 89 | 90 | // find the end of the headers 91 | s = strstr(rawpkt, "\x0d\x0a\x0d\x0a"); 92 | 93 | if (NULL == s) { 94 | debug(1, "Error: missing end of headers"); 95 | return (REQ_ERROR); 96 | } 97 | 98 | s += 4; 99 | 100 | // Correct ping 101 | // Not quite right but gives a good estimate 102 | server->ping_total = (server->ping_total * server->n_requests) / 2; 103 | 104 | debug(3, "processing response..."); 105 | 106 | s = strtok_ret(s, "\x0d\x0a", &linep); 107 | 108 | // NOTE: id=XXX and msg=XXX will be processed by the mod following the one they where the response of 109 | while (NULL != s) { 110 | debug(2, "LINE: %s\n", s); 111 | if (0 == strcmp(s, "{")) { 112 | s = strtok_ret(NULL, "\x0d\x0a", &linep); 113 | continue; 114 | } 115 | 116 | s = strtok_ret(s, "\"", &varp); 117 | key = strtok_ret(NULL, "\"", &varp); 118 | sep = strtok_ret(NULL, " ", &varp); 119 | val = strtok_ret(NULL, "\"", &varp); 120 | if (NULL == val) { 121 | // world etc may be empty which results in NULL val 122 | s = strtok_ret(NULL, "\x0d\x0a", &linep); 123 | continue; 124 | } 125 | //if ( NULL == val && sep 126 | debug(2, "var: '%s' = '%s', sep: '%s'\n", key, val, sep); 127 | if (0 == strcmp(key, "name")) { 128 | server->server_name = strdup(val); 129 | } else if (0 == strcmp(key, "port")) { 130 | port = atoi(val); 131 | change_server_port(server, port, 0); 132 | add_rule(server, key, val, NO_FLAGS); 133 | } else if (0 == strcmp(key, "playercount")) { 134 | server->num_players = atoi(val); 135 | } else if (0 == strcmp(key, "maxplayers")) { 136 | server->max_players = atoi(val); 137 | } else if (0 == strcmp(key, "world")) { 138 | server->map_name = strdup(val); 139 | } else if (0 == strcmp(key, "players")) { 140 | // Players are ", " seperated but potentially player names can have spaces 141 | // so we manually check for the leading space and fix if found 142 | char *playersp; 143 | char *player_name = strtok_ret(val, ",", &playersp); 144 | while (NULL != player_name) { 145 | struct player *player = add_player(server, server->n_player_info); 146 | if (NULL != player) { 147 | if (' ' == player_name[0]) { 148 | player_name++; 149 | } 150 | player->name = strdup(player_name); 151 | debug(4, "Player: %s\n", player->name); 152 | } 153 | player_name = strtok_ret(NULL, ",", &playersp); 154 | } 155 | } else if (0 == strcmp(key, "status")) { 156 | if (200 != atoi(val)) { 157 | server->server_name = DOWN; 158 | return (DONE_FORCE); 159 | } 160 | } else { 161 | add_rule(server, key, val, NO_FLAGS); 162 | } 163 | 164 | s = strtok_ret(NULL, "\x0d\x0a", &linep); 165 | } 166 | 167 | gettimeofday(&server->packet_time1, NULL); 168 | 169 | return (DONE_FORCE); 170 | } 171 | -------------------------------------------------------------------------------- /cube2.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Cube 2 / Sauerbraten protocol 6 | * Copyright 2011 NoisyB 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #include 11 | #include 12 | #include 13 | 14 | #include "debug.h" 15 | #include "qstat.h" 16 | #include "packet_manip.h" 17 | 18 | struct offset { 19 | unsigned char *d; 20 | int pos; 21 | int len; 22 | }; 23 | 24 | //#define SB_MASTER_SERVER "http://sauerbraten.org/masterserver/retrieve.do?item=list" 25 | #define SB_PROTOCOL 258 26 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 27 | #define MAX_ATTR 255 28 | #define MAX_STRING 1024 29 | 30 | static int 31 | getint(struct offset *d) 32 | { 33 | int val = 0; 34 | 35 | if (d->pos >= d->len) { 36 | return (0); 37 | } 38 | 39 | val = d->d[d->pos++] & 0xff;// 8 bit value 40 | 41 | // except... 42 | if ((val == 0x80) && (d->pos < d->len - 2)) { // 16 bit value 43 | val = (d->d[d->pos++] & 0xff); 44 | val |= (d->d[d->pos++] & 0xff) << 8; 45 | } else if ((val == 0x81) && (d->pos < d->len - 4)) { // 32 bit value 46 | val = (d->d[d->pos++] & 0xff); 47 | val |= (d->d[d->pos++] & 0xff) << 8; 48 | val |= (d->d[d->pos++] & 0xff) << 16; 49 | val |= (d->d[d->pos++] & 0xff) << 24; 50 | } 51 | 52 | return (val); 53 | } 54 | 55 | 56 | static char * 57 | getstr(char *dest, int dest_len, struct offset *d) 58 | { 59 | int len = 0; 60 | 61 | if (d->pos >= d->len) { 62 | return (NULL); 63 | } 64 | 65 | len = MIN(dest_len, d->len - d->pos); 66 | strncpy(dest, (const char *)d->d + d->pos, len)[len - 1] = 0; 67 | d->pos += strlen(dest) + 1; 68 | 69 | return (dest); 70 | } 71 | 72 | 73 | static char * 74 | sb_getversion_s(int n) 75 | { 76 | static char *version_s[] = 77 | { 78 | "Justice", 79 | "CTF", 80 | "Assassin", 81 | "Summer", 82 | "Spring", 83 | "Gui", 84 | "Water", 85 | "Normalmap", 86 | "Sp", 87 | "Occlusion", 88 | "Shader", 89 | "Physics", 90 | "Mp", 91 | "", 92 | "Agc", 93 | "Quakecon", 94 | "Independence" 95 | }; 96 | 97 | n = SB_PROTOCOL - n; 98 | if ((n >= 0) && ((size_t)n < sizeof(version_s) / sizeof(version_s[0]))) { 99 | return (version_s[n]); 100 | } 101 | return ("unknown"); 102 | } 103 | 104 | 105 | static char * 106 | sb_getmode_s(int n) 107 | { 108 | static char *mode_s[] = 109 | { 110 | "slowmo SP", 111 | "slowmo DMSP", 112 | "demo", 113 | "SP", 114 | "DMSP", 115 | "ffa/default", 116 | "coopedit", 117 | "ffa/duel", 118 | "teamplay", 119 | "instagib", 120 | "instagib team", 121 | "efficiency", 122 | "efficiency team", 123 | "insta arena", 124 | "insta clan arena", 125 | "tactics arena", 126 | "tactics clan arena", 127 | "capture", 128 | "insta capture", 129 | "regen capture", 130 | "assassin", 131 | "insta assassin", 132 | "ctf", 133 | "insta ctf" 134 | }; 135 | 136 | n += 6; 137 | if ((n >= 0) && ((size_t)n < sizeof(mode_s) / sizeof(mode_s[0]))) { 138 | return (mode_s[n]); 139 | } 140 | return ("unknown"); 141 | } 142 | 143 | 144 | query_status_t 145 | send_cube2_request_packet(struct qserver *server) 146 | { 147 | return (send_packet(server, server->type->status_packet, server->type->status_len)); 148 | } 149 | 150 | 151 | query_status_t 152 | deal_with_cube2_packet(struct qserver *server, char *rawpkt, int pktlen) 153 | { 154 | // skip unimplemented ack, crc, etc 155 | int i; 156 | int numattr; 157 | int attr[MAX_ATTR]; 158 | char buf[MAX_STRING]; 159 | 160 | enum { 161 | MM_OPEN = 0, 162 | MM_VETO, 163 | MM_LOCKED, 164 | MM_PRIVATE 165 | }; 166 | 167 | struct offset d; 168 | d.d = (unsigned char *)rawpkt; 169 | d.pos = 0; 170 | d.len = pktlen; 171 | 172 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 173 | getint(&d); // we have the ping already 174 | server->num_players = getint(&d); 175 | numattr = getint(&d); 176 | for (i = 0; i < numattr && i < MAX_ATTR; i++) { 177 | attr[i] = getint(&d); 178 | } 179 | 180 | server->protocol_version = attr[0]; 181 | 182 | sprintf(buf, "%d %s", attr[0], sb_getversion_s(attr[0])); 183 | add_rule(server, "version", buf, NO_FLAGS); 184 | 185 | sprintf(buf, "%d %s", attr[1], sb_getmode_s(attr[1])); 186 | add_rule(server, "mode", buf, NO_FLAGS); 187 | 188 | sprintf(buf, "%d", attr[2]); 189 | add_rule(server, "seconds_left", buf, NO_FLAGS); 190 | 191 | server->max_players = attr[3]; 192 | 193 | switch (attr[5]) { 194 | case MM_OPEN: 195 | sprintf(buf, "%d open", attr[5]); 196 | break; 197 | 198 | case MM_VETO: 199 | sprintf(buf, "%d veto", attr[5]); 200 | break; 201 | 202 | case MM_LOCKED: 203 | sprintf(buf, "%d locked", attr[5]); 204 | break; 205 | 206 | case MM_PRIVATE: 207 | sprintf(buf, "%d private", attr[5]); 208 | break; 209 | 210 | default: 211 | sprintf(buf, "%d unknown", attr[5]); 212 | } 213 | add_rule(server, "mm", buf, NO_FLAGS); 214 | 215 | for (i = 0; i < numattr && i < MAX_ATTR; i++) { 216 | char buf2[MAX_STRING]; 217 | sprintf(buf, "attr%d", i); 218 | sprintf(buf2, "%d", attr[i]); 219 | add_rule(server, buf, buf2, NO_FLAGS); 220 | } 221 | 222 | getstr(buf, MAX_STRING, &d); 223 | server->map_name = strdup(buf); 224 | getstr(buf, MAX_STRING, &d); 225 | server->server_name = strdup(buf); 226 | 227 | return (DONE_FORCE); 228 | } 229 | -------------------------------------------------------------------------------- /info/a2s.txt: -------------------------------------------------------------------------------- 1 | Server Queries 2 | 3 | The Source engine allows you to query information from a running game server 4 | using UDP/IP packets. This document describes the packet formats and protocol 5 | to access this data. 6 | 7 | Basic Data Types 8 | 9 | All server queries consist of 5 basic types of data packed together into a data 10 | stream. All types are little endian. 11 | 12 | Name Description 13 | 14 | byte 8 bit character 15 | 16 | short 16 bit signed integer 17 | 18 | long 32 bit signed integer 19 | 20 | float 32 bit float value 21 | 22 | string variable length byte field, terminated by 0x00 23 | 24 | Query Types 25 | 26 | The server responds to 4 queries: 27 | 28 | • A2S_SERVERQUERY_GETCHALLENGE - Returns a challenge number for use in the 29 | player and rules query. 30 | • A2S_INFO - Basic information about the server. 31 | • A2S_PLAYER - Details about each player on the server. 32 | • A2S_RULES - The rules the server is using. 33 | 34 | Queries should be sent in UDP packets to the listen port of the server, which 35 | is typically port 27015. 36 | 37 | A2S_SERVERQUERY_GETCHALLENGE 38 | 39 | Request format 40 | 41 | Challenge values are required for A2S_PLAYER and A2S_RULES requests, you can 42 | use this request to get one. 43 | Note: You can also send A2S_PLAYER and A2S_RULES queries with a challenge value 44 | of -1 (0xFF FF FF FF FF FF FF FF) and they will respond with a challenge value 45 | to use (using the reply format below). 46 | 47 | FF FF FF FF 57 48 | 49 | Reply format 50 | 51 | Data Type Comment 52 | 53 | Type byte Should be equal to 'A' (0x41) 54 | 55 | Challenge long The challenge number to use 56 | 57 | Example reply: 58 | 59 | FF FF FF FF FF 41 32 42 59 45 53 93 43 71 60 | 61 | A2S_INFO 62 | 63 | Request format 64 | 65 | Server info can be requested by sending the following byte values in a UDP 66 | packet to the server. 67 | 68 | FF FF FF FF 54 53 6F 75 72 63 65 20 45 6E 67 69 6E 65 20 51 75 65 72 79 00 69 | 70 | Reply format 71 | 72 | Data Type Comment 73 | 74 | Type byte Should be equal to 'I' (0x49) 75 | 76 | Version byte Network version 77 | 78 | Hostname string The Servers name 79 | 80 | Map string The current map being played 81 | 82 | Game Directory string The Game type 83 | 84 | Game string A friendly string name for the game type 85 | Description 86 | 87 | AppID short Steam Application number (currently always set to 0) 88 | 89 | Num players byte The number of players currently on the server 90 | 91 | Max players byte Maximum allowed players for the server 92 | 93 | Num of bots byte Number of bot players currently on the server 94 | 95 | Dedicated byte Set to 1 for dedicated servers 96 | 97 | OS byte 'l' for Linux, 'w' for Windows 98 | 99 | Password byte If set to 1 a password is required to join this server 100 | 101 | Secure byte If set to 1 this server is running VAC 102 | 103 | Game Version string The version of the game 104 | 105 | Example reply: 106 | 107 | FF FF FF FF 49 02 67 61 6D 65 32 78 73 2E 63 6F ....I.game2xs.co 108 | 6D 20 43 6F 75 6E 74 65 72 2D 53 74 72 69 6B 65 m.Counter-Strike 109 | 20 53 6F 75 72 63 65 20 23 31 00 64 65 5F 64 75 .Source.#1.de_du 110 | 73 74 00 63 73 74 72 69 6B 65 00 43 6F 75 6E 74 st.cstrike.Count 111 | 65 72 2D 53 74 72 69 6B 65 00 00 00 0B 28 00 64 er-Strike....(.d 112 | 6C 00 00 31 2e 31 2e 30 2e 31 36 00 l..1.1.0.16 113 | 114 | A2S_PLAYER 115 | 116 | Request format 117 | 118 | FF FF FF FF 55 <4 byte challenge number> 119 | 120 | The challenge number can either be set to -1 (0xFF FF FF FF FF FF FF FF) to 121 | have the server reply with S2C_CHALLENGE, or use the value from a previous 122 | A2S_SERVERQUERY_GETCHALLENGE request. 123 | 124 | Reply format 125 | 126 | The players response has two sections, the initial header: 127 | 128 | Data Type Comment 129 | 130 | Type byte Should be equal to 'D' (0x44) 131 | 132 | Num Players byte The number of players reported in this response 133 | 134 | 135 | Then for each player the following fields are sent: 136 | 137 | Data Type Comment 138 | 139 | Index byte The index into [0.. Num Players] for this entry 140 | 141 | Player Name string Player's name 142 | 143 | Kills long Number of kills this player has 144 | 145 | Time connected float The time in seconds this player has been connected 146 | 147 | 148 | A2S_RULES 149 | 150 | Request format 151 | 152 | FF FF FF FF 56 <4 byte challenge number> 153 | 154 | The challenge number can either be set to -1 (0xFF FF FF FF FF FF FF FF) to 155 | have the server reply with S2C_CHALLENGE, or use the value from a previous 156 | A2S_SERVERQUERY_GETCHALLENGE request. 157 | 158 | Reply format 159 | 160 | The rules response has two sections, the initial header: 161 | 162 | Data Type Comment 163 | 164 | Type byte Should be equal to 'E' (0x45) 165 | 166 | Num Rules short The number of rules reported in this response 167 | 168 | 169 | Then for each rule the following fields are sent: 170 | 171 | Data Type Comment 172 | 173 | Rule Name string The name of the rule 174 | 175 | Rule Value string The rules value 176 | 177 | 178 | 179 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 180 | 181 | (C) 2004 Valve Corporation. All rights reserved. Valve, the Valve logo, 182 | Half-Life, the Half-Life logo, the Lambda logo, Steam, the Steam logo, Team 183 | Fortress, the Team Fortress logo, Opposing Force, Day of Defeat, the Day of 184 | Defeat logo, Counter-Strike, the Counter-Strike logo, Source, the Source logo, 185 | Hammer and Counter-Strike: Condition Zero are trademarks and/or registered 186 | trademarks of Valve Corporation. Microsoft and Visual Studio are trademarks 187 | and/or registered trademarks of Microsoft Corporation. All other trademarks 188 | are property of their respective owners. 189 | 190 | -------------------------------------------------------------------------------- /farmsim.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Crysis query protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | #include 21 | #include 22 | #include 23 | 24 | #include "debug.h" 25 | #include "utils.h" 26 | #include "qstat.h" 27 | #include "md5.h" 28 | #include "packet_manip.h" 29 | 30 | char * 31 | decode_farmsim_val(char *val) 32 | { 33 | // Very basic html conversion 34 | val = str_replace(val, """, "\""); 35 | return (str_replace(val, "&", "&")); 36 | } 37 | 38 | 39 | query_status_t 40 | send_farmsim_request_packet(struct qserver *server) 41 | { 42 | char buf[256], *code; 43 | 44 | server->saved_data.pkt_max = -1; 45 | code = get_param_value(server, "code", ""); 46 | sprintf(buf, "GET /feed/dedicated-server-stats.xml?code=%s HTTP/1.1\015\012User-Agent: qstat\015\012\015\012", code); 47 | 48 | return (send_packet(server, buf, strlen(buf))); 49 | } 50 | 51 | 52 | query_status_t 53 | valid_farmsim_response(struct qserver *server, char *rawpkt, int pktlen) 54 | { 55 | char *s; 56 | int len; 57 | int cnt = packet_count(server); 58 | 59 | if ((0 == cnt) && (0 != strncmp("HTTP/1.1 200 OK", rawpkt, 15))) { 60 | // not valid response 61 | debug(2, "Invalid"); 62 | return (REQ_ERROR); 63 | } 64 | 65 | s = strnstr(rawpkt, "Content-Length: ", pktlen); 66 | if (s == NULL) { 67 | // not valid response 68 | debug(2, "Invalid (no content-length)"); 69 | return (INPROGRESS); 70 | } 71 | s += 16; 72 | 73 | // TODO: remove this bug work around 74 | if (*s == ':') { 75 | s += 2; 76 | } 77 | if (sscanf(s, "%d", &len) != 1) { 78 | debug(2, "Invalid (no length)"); 79 | return (INPROGRESS); 80 | } 81 | 82 | s = strnstr(rawpkt, "\015\012\015\012", pktlen); 83 | if (s == NULL) { 84 | debug(2, "Invalid (no end of header"); 85 | return (INPROGRESS); 86 | } 87 | 88 | s += 4; 89 | if (pktlen != (s - rawpkt + len)) { 90 | debug(2, "Outstanding data"); 91 | return (INPROGRESS); 92 | } 93 | 94 | debug(2, "Valid data"); 95 | return (DONE_FORCE); 96 | } 97 | 98 | 99 | char * 100 | farmsim_xml_attrib(char *line, char *name) 101 | { 102 | char *q, *p, *val; 103 | 104 | p = strstr(line, name); 105 | if (p == NULL) { 106 | return (NULL); 107 | } 108 | 109 | p += strlen(name); 110 | if (strlen(p) < 4) { 111 | return (NULL); 112 | } 113 | p += 2; 114 | 115 | q = strchr(p, '"'); 116 | if (q == NULL) { 117 | return (NULL); 118 | } 119 | *q = '\0'; 120 | 121 | val = strdup(p); 122 | *q = '"'; 123 | debug(4, "%s = %s", name, val); 124 | 125 | return (val); 126 | } 127 | 128 | 129 | query_status_t 130 | deal_with_farmsim_packet(struct qserver *server, char *rawpkt, int pktlen) 131 | { 132 | char *s, *val, *line; 133 | query_status_t state = INPROGRESS; 134 | 135 | debug(2, "processing..."); 136 | 137 | if (!server->combined) { 138 | state = valid_farmsim_response(server, rawpkt, pktlen); 139 | server->retry1 = n_retries; 140 | if (server->n_requests == 0) { 141 | server->ping_total = time_delta(&packet_recv_time, &server->packet_time1); 142 | server->n_requests++; 143 | } 144 | 145 | switch (state) { 146 | case INPROGRESS: 147 | { 148 | // response fragment recieved 149 | int pkt_id; 150 | int pkt_max; 151 | 152 | // We're expecting more to come 153 | debug(5, "fragment recieved..."); 154 | pkt_id = packet_count(server); 155 | pkt_max = pkt_id + 1; 156 | if (!add_packet(server, 0, pkt_id, pkt_max, pktlen, rawpkt, 1)) { 157 | // fatal error e.g. out of memory 158 | return (MEM_ERROR); 159 | } 160 | 161 | // combine_packets will call us recursively 162 | return (combine_packets(server)); 163 | } 164 | 165 | case DONE_FORCE: 166 | break; // single packet response fall through 167 | 168 | default: 169 | return (state); 170 | } 171 | } 172 | 173 | if (state != DONE_FORCE) { 174 | state = valid_farmsim_response(server, rawpkt, pktlen); 175 | switch (state) { 176 | case DONE_FORCE: 177 | break; // actually process 178 | 179 | default: 180 | return (state); 181 | } 182 | } 183 | 184 | // Correct ping 185 | // Not quite right but gives a good estimate 186 | server->ping_total = (server->ping_total * server->n_requests) / 2; 187 | 188 | debug(3, "processing response..."); 189 | 190 | s = rawpkt; 191 | // Ensure we're null terminated (will only loose the last \x0a) 192 | s[pktlen - 1] = '\0'; 193 | s = decode_farmsim_val(s); 194 | line = strtok(s, "\012"); 195 | 196 | // NOTE: id=XXX and msg=XXX will be processed by the mod following the one they where the response of 197 | while (line != NULL) { 198 | debug(4, "LINE: %s\n", line); 199 | if (strstr(line, " 210 | 211 | // Server Name 212 | val = farmsim_xml_attrib(line, "name"); 213 | if (val != NULL) { 214 | server->server_name = val; 215 | } else { 216 | server->server_name = strdup("Unknown"); 217 | } 218 | 219 | // Map Name 220 | val = farmsim_xml_attrib(line, "mapName"); 221 | if (val != NULL) { 222 | server->map_name = val; 223 | } else { 224 | server->map_name = strdup("Default"); 225 | } 226 | } else if (strstr(line, " 228 | debug(1, "max_players = atoi(val); 234 | free(val); 235 | } else { 236 | server->max_players = get_param_ui_value(server, "maxplayers", 1); 237 | } 238 | // Num Players 239 | val = farmsim_xml_attrib(line, "numUsed"); 240 | if (val != NULL) { 241 | server->num_players = atoi(val); 242 | free(val); 243 | } else { 244 | server->num_players = 0; 245 | } 246 | } 247 | 248 | line = strtok(NULL, "\012"); 249 | } 250 | 251 | gettimeofday(&server->packet_time1, NULL); 252 | 253 | return (DONE_FORCE); 254 | } 255 | -------------------------------------------------------------------------------- /ksp.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * KSP query protocol 6 | * Copyright 2014 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | #include 21 | #include 22 | #include 23 | 24 | #include "debug.h" 25 | #include "utils.h" 26 | #include "qstat.h" 27 | #include "md5.h" 28 | #include "packet_manip.h" 29 | 30 | char * 31 | decode_ksp_val(char *val) 32 | { 33 | // Very basic html conversion 34 | val = str_replace(val, """, "\""); 35 | return (str_replace(val, "&", "&")); 36 | } 37 | 38 | 39 | query_status_t 40 | send_ksp_request_packet(struct qserver *server) 41 | { 42 | char buf[256]; 43 | 44 | server->saved_data.pkt_max = -1; 45 | sprintf(buf, "GET / HTTP/1.1\015\012User-Agent: qstat\015\012Host: %s:%d\015\012\015\012", server->host_name, server->port); 46 | 47 | return (send_packet(server, buf, strlen(buf))); 48 | } 49 | 50 | 51 | query_status_t 52 | valid_ksp_response(struct qserver *server, char *rawpkt, int pktlen) 53 | { 54 | char *s; 55 | int len; 56 | int cnt = packet_count(server); 57 | 58 | if ((0 == cnt) && (0 != strncmp("HTTP/1.1 200 OK", rawpkt, 15))) { 59 | // not valid response 60 | debug(2, "Invalid"); 61 | return (REQ_ERROR); 62 | } 63 | 64 | s = strnstr(rawpkt, "Content-Length: ", pktlen); 65 | if (s == NULL) { 66 | // not valid response 67 | debug(2, "Invalid (no content-length)"); 68 | return (INPROGRESS); 69 | } 70 | s += 16; 71 | 72 | // TODO: remove this bug work around 73 | if (*s == ':') { 74 | s += 2; 75 | } 76 | if (sscanf(s, "%d", &len) != 1) { 77 | debug(2, "Invalid (no length)"); 78 | return (INPROGRESS); 79 | } 80 | 81 | s = strnstr(rawpkt, "\015\012\015\012", pktlen); 82 | if (s == NULL) { 83 | debug(2, "Invalid (no end of header"); 84 | return (INPROGRESS); 85 | } 86 | 87 | s += 4; 88 | if (pktlen != (s - rawpkt + len)) { 89 | debug(2, "Outstanding data"); 90 | return (INPROGRESS); 91 | } 92 | 93 | debug(2, "Valid data"); 94 | return (DONE_FORCE); 95 | } 96 | 97 | 98 | char * 99 | ksp_json_attrib(char *line, char *name) 100 | { 101 | char *q, *p, *val; 102 | 103 | p = strstr(line, name); 104 | if (p == NULL) { 105 | return (NULL); 106 | } 107 | 108 | p += strlen(name); 109 | if (strlen(p) < 3) { 110 | return (NULL); 111 | } 112 | p += 2; 113 | if (*p == '"') { 114 | // String 115 | p++; 116 | q = strchr(p, '"'); 117 | if (q == NULL) { 118 | return (NULL); 119 | } 120 | } else { 121 | // Integer, bool etc 122 | q = strchr(p, ','); 123 | if (q == NULL) { 124 | return (NULL); 125 | } 126 | } 127 | *q = '\0'; 128 | 129 | val = strdup(p); 130 | *q = '"'; 131 | debug(4, "%s = %s", name, val); 132 | 133 | return (val); 134 | } 135 | 136 | 137 | query_status_t 138 | deal_with_ksp_packet(struct qserver *server, char *rawpkt, int pktlen) 139 | { 140 | char *s, *val, *line; 141 | query_status_t state = INPROGRESS; 142 | 143 | debug(2, "processing..."); 144 | 145 | if (!server->combined) { 146 | state = valid_ksp_response(server, rawpkt, pktlen); 147 | server->retry1 = n_retries; 148 | if (server->n_requests == 0) { 149 | server->ping_total = time_delta(&packet_recv_time, &server->packet_time1); 150 | server->n_requests++; 151 | } 152 | 153 | switch (state) { 154 | case INPROGRESS: 155 | { 156 | // response fragment recieved 157 | int pkt_id; 158 | int pkt_max; 159 | 160 | // We're expecting more to come 161 | debug(5, "fragment recieved..."); 162 | pkt_id = packet_count(server); 163 | pkt_max = pkt_id + 1; 164 | if (!add_packet(server, 0, pkt_id, pkt_max, pktlen, rawpkt, 1)) { 165 | // fatal error e.g. out of memory 166 | return (MEM_ERROR); 167 | } 168 | 169 | // combine_packets will call us recursively 170 | return (combine_packets(server)); 171 | } 172 | 173 | case DONE_FORCE: 174 | break; // single packet response fall through 175 | 176 | default: 177 | return (state); 178 | } 179 | } 180 | 181 | if (state != DONE_FORCE) { 182 | state = valid_ksp_response(server, rawpkt, pktlen); 183 | switch (state) { 184 | case DONE_FORCE: 185 | break; // actually process 186 | 187 | default: 188 | return (state); 189 | } 190 | } 191 | 192 | // Correct ping 193 | // Not quite right but gives a good estimate 194 | server->ping_total = (server->ping_total * server->n_requests) / 2; 195 | 196 | debug(3, "processing response..."); 197 | 198 | s = rawpkt; 199 | // Ensure we're null terminated (will only loose the last \x0a) 200 | s[pktlen - 1] = '\0'; 201 | s = decode_ksp_val(s); 202 | line = strtok(s, "\012"); 203 | 204 | // NOTE: id=XXX and msg=XXX will be processed by the mod following the one they where the response of 205 | while (line != NULL) { 206 | debug(4, "LINE: %s\n", line); 207 | if (strstr(line, "{") != NULL) { 208 | debug(1, "{..."); 209 | // { 210 | // "cheats":true, 211 | // "game_mode":"SANDBOX", 212 | // "lastPlayerActivity":81403, 213 | // "max_players":12, 214 | // "modControlSha":"e46569487926a3273f58e06a080b0747b0ae702ec1877906511fe2c29816528b", 215 | // "mod_control":1, 216 | // "player_count":0, 217 | // "players":"", 218 | // "port":6752, 219 | // "protocol_version":25, 220 | // "server_name":"Multiplay :: Online - Clanserver", 221 | // "universeSize":96576, 222 | // "version":"v0.1.5.6" 223 | // } 224 | 225 | // Server Name 226 | val = ksp_json_attrib(line, "server_name"); 227 | if (val != NULL) { 228 | server->server_name = val; 229 | } else { 230 | server->server_name = strdup("Unknown"); 231 | } 232 | 233 | // Map Name 234 | val = ksp_json_attrib(line, "mapName"); 235 | if (val != NULL) { 236 | server->map_name = val; 237 | } else { 238 | server->map_name = strdup("Default"); 239 | } 240 | 241 | // Max Players 242 | val = ksp_json_attrib(line, "max_players"); 243 | if (val != NULL) { 244 | server->max_players = atoi(val); 245 | free(val); 246 | } else { 247 | server->max_players = get_param_ui_value(server, "max_players", 1); 248 | } 249 | 250 | // Num Players 251 | val = ksp_json_attrib(line, "player_count"); 252 | if (val != NULL) { 253 | server->num_players = atoi(val); 254 | free(val); 255 | } else { 256 | server->num_players = 0; 257 | } 258 | } 259 | 260 | line = strtok(NULL, "\012"); 261 | } 262 | 263 | gettimeofday(&server->packet_time1, NULL); 264 | 265 | return (DONE_FORCE); 266 | } 267 | -------------------------------------------------------------------------------- /starmade.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * StarMade query protocol 6 | * Copyright 2013 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | #include 21 | #include 22 | #include 23 | 24 | #include "debug.h" 25 | #include "qstat.h" 26 | #include "utils.h" 27 | #include "packet_manip.h" 28 | 29 | typedef enum { 30 | SM_INT = 0x01, 31 | SM_LONG, 32 | SM_FLOAT, 33 | SM_STRING, 34 | SM_BOOLEAN, 35 | SM_BYTE, 36 | SM_SHORT, 37 | SM_BYTE_ARRAY 38 | } starmade_param_type; 39 | 40 | query_status_t 41 | send_starmade_request_packet(struct qserver *server) 42 | { 43 | char buf[13] = { 0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00 }; 44 | 45 | /* 46 | * Header 47 | * int(4) - packet length 48 | * byte - packet type : 0x2a = packet, 0x20 = ping, test = 0x64, logout = 0x41 49 | * short(2) - packet id 50 | * byte - command id : 0x01 = status, 0x02 = exec 51 | * byte - command type : 0x6f = parameterised command, 0x84 = stream command 52 | * 53 | * Paramters 54 | * int(4) - paramter count 55 | * byte - paramter type : 1 = int, 2 = long, 3 = float, 4 = UTF8 string, 5 = boolean, 6 = byte, 7 = short, 8 = byte array 56 | * ... 57 | */ 58 | 59 | debug(3, "send_starmade_request_packet: state = %ld", server->challenge); 60 | 61 | return (send_packet(server, buf, 13)); 62 | } 63 | 64 | 65 | static query_status_t 66 | starmade_read_parameter(char **datap, int *datalen, void *val, int vlen, starmade_param_type type) 67 | { 68 | int size; 69 | 70 | if (**datap != type) { 71 | /* unexpected type */ 72 | debug(2, "Invalid type detected, expected 0x%02x got 0x%02x", type, **datap); 73 | return (REQ_ERROR); 74 | } 75 | 76 | (*datap)++; 77 | (*datalen)--; 78 | 79 | switch (type) { 80 | case SM_INT: 81 | size = 4; 82 | if ((size > vlen) || (size > *datalen)) { 83 | return (MEM_ERROR); 84 | } 85 | *(uint32_t *)val = ((uint32_t)(*datap)[3]) | 86 | ((uint32_t)(*datap)[2] << 8) | 87 | ((uint32_t)(*datap)[1] << 16) | 88 | ((uint32_t)(*datap)[0] << 24); 89 | break; 90 | 91 | case SM_LONG: 92 | size = 8; 93 | if ((size > vlen) || (size > *datalen)) { 94 | return (MEM_ERROR); 95 | } 96 | *(uint64_t *)val = ((uint64_t)(*datap)[7]) | 97 | ((uint64_t)(*datap)[6] << 8) | 98 | ((uint64_t)(*datap)[5] << 16) | 99 | ((uint64_t)(*datap)[4] << 24) | 100 | ((uint64_t)(*datap)[3] << 32) | 101 | ((uint64_t)(*datap)[2] << 40) | 102 | ((uint64_t)(*datap)[1] << 48) | 103 | ((uint64_t)(*datap)[0] << 56); 104 | break; 105 | 106 | case SM_FLOAT: 107 | size = 4; 108 | if ((size > vlen) || (size > *datalen)) { 109 | return (MEM_ERROR); 110 | } 111 | *(uint32_t *)val = (uint32_t)(*datap)[3] | 112 | ((uint32_t)(*datap)[2] << 8) | 113 | ((uint32_t)(*datap)[1] << 16) | 114 | ((uint32_t)(*datap)[0] << 24); 115 | break; 116 | 117 | case SM_STRING: 118 | size = ((short)(*datap)[1]) | ((short)(*datap)[0] << 8); 119 | if ((size > vlen) || (size > *datalen)) { 120 | return (MEM_ERROR); 121 | } 122 | (*datap) += 2; 123 | memcpy(val, *datap, size); 124 | ((char *)val)[size] = 0x00; 125 | break; 126 | 127 | case SM_BOOLEAN: 128 | size = 1; 129 | if ((size > vlen) || (size > *datalen)) { 130 | return (MEM_ERROR); 131 | } 132 | *(unsigned char *)val = **datap; 133 | break; 134 | 135 | case SM_BYTE: 136 | size = 1; 137 | if ((size > vlen) || (size > *datalen)) { 138 | return (MEM_ERROR); 139 | } 140 | *(unsigned char *)val = **datap; 141 | break; 142 | 143 | case SM_SHORT: 144 | size = 2; 145 | if ((size > vlen) || (size > *datalen)) { 146 | return (MEM_ERROR); 147 | } 148 | *(short *)val = (short)(*datap)[1] | ((short)(*datap)[0] << 8); 149 | break; 150 | 151 | case SM_BYTE_ARRAY: 152 | debug(2, "Unsupport type 0x%02x requested / received", type); 153 | return (REQ_ERROR); 154 | 155 | default: 156 | debug(2, "Unknown type 0x%02x requested / received", type); 157 | return (REQ_ERROR); 158 | } 159 | 160 | (*datap) += size; 161 | (*datalen) -= size; 162 | 163 | return (INPROGRESS); 164 | } 165 | 166 | 167 | query_status_t 168 | deal_with_starmade_packet(struct qserver *server, char *rawpkt, int pktlen) 169 | { 170 | char *s, buf[256]; 171 | int ret; 172 | 173 | debug(2, "processing..."); 174 | 175 | if (pktlen < 54) { 176 | // Invalid password 177 | return (REQ_ERROR); 178 | } 179 | 180 | s = rawpkt; 181 | 182 | // TODO: continue reading / combining packets until we've read enough. 183 | debug(3, "packet: combined = %d, challenge = %ld", server->combined, server->challenge); 184 | // Packet Header 185 | // int - response size excluding header (12 bytes) 186 | s += 4; 187 | // long - ts?? 188 | s += 8; 189 | 190 | // Response Header 191 | // TODO: validate header details 192 | // byte - packet type 193 | s += 1; 194 | // short - packet id 195 | s += 2; 196 | // byte - command id 197 | s += 1; 198 | // byte - command type 199 | s += 1; 200 | 201 | // Parameters 202 | // int - paramater count 203 | s += 4; 204 | 205 | // byte - info version 206 | ret = starmade_read_parameter(&s, &pktlen, buf, sizeof(buf), SM_BYTE); 207 | if (ret != INPROGRESS) { 208 | return (ret); 209 | } 210 | 211 | // float - version 212 | ret = starmade_read_parameter(&s, &pktlen, buf, sizeof(buf), SM_FLOAT); 213 | if (ret != INPROGRESS) { 214 | return (ret); 215 | } 216 | 217 | // string - server name 218 | ret = starmade_read_parameter(&s, &pktlen, buf, sizeof(buf), SM_STRING); 219 | if (ret != INPROGRESS) { 220 | return (ret); 221 | } 222 | server->server_name = strdup(buf); 223 | 224 | // string - description 225 | ret = starmade_read_parameter(&s, &pktlen, buf, sizeof(buf), SM_STRING); 226 | if (ret != INPROGRESS) { 227 | return (ret); 228 | } 229 | 230 | // long - start time 231 | ret = starmade_read_parameter(&s, &pktlen, buf, sizeof(buf), SM_LONG); 232 | if (ret != INPROGRESS) { 233 | return (ret); 234 | } 235 | 236 | // int - player count 237 | ret = starmade_read_parameter(&s, &pktlen, &server->num_players, sizeof(server->num_players), SM_INT); 238 | if (ret != INPROGRESS) { 239 | return (ret); 240 | } 241 | 242 | // int - max players 243 | ret = starmade_read_parameter(&s, &pktlen, &server->max_players, sizeof(server->max_players), SM_INT); 244 | if (ret != INPROGRESS) { 245 | return (ret); 246 | } 247 | 248 | gettimeofday(&server->packet_time1, NULL); 249 | 250 | server->map_name = strdup("default"); 251 | 252 | return (DONE_FORCE); 253 | } 254 | -------------------------------------------------------------------------------- /crysis.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Crysis query protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | #include 21 | #include 22 | #include 23 | 24 | #include "debug.h" 25 | #include "utils.h" 26 | #include "qstat.h" 27 | #include "md5.h" 28 | #include "packet_manip.h" 29 | 30 | char * 31 | decode_crysis_val(char *val) 32 | { 33 | // Very basic html conversion 34 | val = str_replace(val, """, "\""); 35 | return (str_replace(val, "&", "&")); 36 | } 37 | 38 | 39 | query_status_t 40 | send_crysis_request_packet(struct qserver *server) 41 | { 42 | char cmd[256], buf[1024], *password, *md5; 43 | 44 | debug(2, "challenge: %ld", server->challenge); 45 | switch (server->challenge) { 46 | case 0: 47 | // Not seen a challenge yet, request it 48 | server->challenge++; 49 | sprintf(cmd, "challenge"); 50 | break; 51 | 52 | case 1: 53 | server->challenge++; 54 | password = get_param_value(server, "password", ""); 55 | sprintf(cmd, "%s:%s", server->challenge_string, password); 56 | md5 = md5_hex(cmd, strlen(cmd)); 57 | sprintf(cmd, "authenticate %s", md5); 58 | free(md5); 59 | break; 60 | 61 | case 2: 62 | // NOTE: we currently don't support player info 63 | server->challenge++; 64 | server->flags |= TF_STATUS_QUERY; 65 | server->n_servers = 3; 66 | sprintf(cmd, "status"); 67 | break; 68 | 69 | case 3: 70 | return (DONE_FORCE); 71 | } 72 | 73 | server->saved_data.pkt_max = -1; 74 | sprintf(buf, "POST /RPC2 HTTP/1.1\015\012Keep-Alive: 300\015\012User-Agent: qstat %s\015\012Content-Length: %d\015\012Content-Type: text/xml\015\012\015\012%s", QSTAT_VERSION, (int)(98 + strlen(cmd)), cmd); 75 | 76 | return (send_packet(server, buf, strlen(buf))); 77 | } 78 | 79 | 80 | query_status_t 81 | valid_crysis_response(struct qserver *server, char *rawpkt, int pktlen) 82 | { 83 | char *s; 84 | int len; 85 | int cnt = packet_count(server); 86 | 87 | if ((0 == cnt) && (0 != strncmp("HTTP/1.1 200 OK", rawpkt, 15))) { 88 | // not valid response 89 | return (REQ_ERROR); 90 | } 91 | 92 | s = strnstr(rawpkt, "Content-Length: ", pktlen); 93 | if (NULL == s) { 94 | // not valid response 95 | return (INPROGRESS); 96 | } 97 | s += 16; 98 | if (1 != sscanf(s, "%d", &len)) { 99 | return (INPROGRESS); 100 | } 101 | 102 | s = strnstr(rawpkt, "\015\012\015\012", pktlen); 103 | if (NULL == s) { 104 | return (INPROGRESS); 105 | } 106 | 107 | s += 4; 108 | if (pktlen != (s - rawpkt + len)) { 109 | return (INPROGRESS); 110 | } 111 | 112 | return (DONE_FORCE); 113 | } 114 | 115 | 116 | char * 117 | crysis_response(struct qserver *server, char *rawpkt, int pktlen) 118 | { 119 | char *s, *e; 120 | int len = pktlen; 121 | 122 | s = strnstr(rawpkt, "", len); 123 | if (NULL == s) { 124 | // not valid response 125 | return (NULL); 126 | } 127 | s += 46; 128 | len += rawpkt - s; 129 | e = strnstr(s, "", len); 130 | if (NULL == e) { 131 | // not valid response 132 | return (NULL); 133 | } 134 | *e = '\0'; 135 | 136 | return (strdup(s)); 137 | } 138 | 139 | 140 | query_status_t 141 | deal_with_crysis_packet(struct qserver *server, char *rawpkt, int pktlen) 142 | { 143 | char *s, *val, *line; 144 | query_status_t state = INPROGRESS; 145 | 146 | debug(2, "processing..."); 147 | 148 | if (!server->combined) { 149 | state = valid_crysis_response(server, rawpkt, pktlen); 150 | server->retry1 = n_retries; 151 | if (0 == server->n_requests) { 152 | server->ping_total = time_delta(&packet_recv_time, &server->packet_time1); 153 | server->n_requests++; 154 | } 155 | 156 | switch (state) { 157 | case INPROGRESS: 158 | { 159 | // response fragment recieved 160 | int pkt_id; 161 | int pkt_max; 162 | 163 | // We're expecting more to come 164 | debug(5, "fragment recieved..."); 165 | pkt_id = packet_count(server); 166 | pkt_max = pkt_id++; 167 | if (!add_packet(server, 0, pkt_id, pkt_max, pktlen, rawpkt, 1)) { 168 | // fatal error e.g. out of memory 169 | return (MEM_ERROR); 170 | } 171 | 172 | // combine_packets will call us recursively 173 | return (combine_packets(server)); 174 | } 175 | 176 | case DONE_FORCE: 177 | break; // single packet response fall through 178 | 179 | default: 180 | return (state); 181 | } 182 | } 183 | 184 | if (DONE_FORCE != state) { 185 | state = valid_crysis_response(server, rawpkt, pktlen); 186 | switch (state) { 187 | case DONE_FORCE: 188 | break; // actually process 189 | 190 | default: 191 | return (state); 192 | } 193 | } 194 | 195 | debug(3, "packet: challenge = %ld", server->challenge); 196 | s = NULL; 197 | switch (server->challenge) { 198 | case 1: 199 | s = crysis_response(server, rawpkt, pktlen); 200 | if (NULL != s) { 201 | server->challenge_string = s; 202 | return (send_crysis_request_packet(server)); 203 | } 204 | return (REQ_ERROR); 205 | 206 | case 2: 207 | s = crysis_response(server, rawpkt, pktlen); 208 | if (NULL == s) { 209 | return (REQ_ERROR); 210 | } 211 | if (0 != strncmp(s, "authorized", 10)) { 212 | free(s); 213 | return (REQ_ERROR); 214 | } 215 | free(s); 216 | return (send_crysis_request_packet(server)); 217 | 218 | case 3: 219 | s = crysis_response(server, rawpkt, pktlen); 220 | if (NULL == s) { 221 | return (REQ_ERROR); 222 | } 223 | } 224 | 225 | // Correct ping 226 | // Not quite right but gives a good estimate 227 | server->ping_total = (server->ping_total * server->n_requests) / 2; 228 | 229 | debug(3, "processing response..."); 230 | 231 | s = decode_crysis_val(s); 232 | line = strtok(s, "\012"); 233 | 234 | // NOTE: id=XXX and msg=XXX will be processed by the mod following the one they where the response of 235 | while (NULL != line) { 236 | debug(4, "LINE: %s\n", line); 237 | val = strstr(line, ":"); 238 | if (NULL != val) { 239 | *val = '\0'; 240 | val += 2; 241 | debug(4, "var: %s, val: %s", line, val); 242 | if (0 == strcmp("name", line)) { 243 | server->server_name = strdup(val); 244 | } else if (0 == strcmp("level", line)) { 245 | server->map_name = strdup(val); 246 | } else if (0 == strcmp("players", line)) { 247 | if (2 == sscanf(val, "%d/%d", &server->num_players, &server->max_players)) { 248 | } 249 | } else if ( 250 | (0 == strcmp("version", line)) || 251 | (0 == strcmp("gamerules", line)) || 252 | (0 == strcmp("time remaining", line)) 253 | ) { 254 | add_rule(server, line, val, NO_FLAGS); 255 | } 256 | } 257 | 258 | line = strtok(NULL, "\012"); 259 | } 260 | 261 | gettimeofday(&server->packet_time1, NULL); 262 | 263 | return (DONE_FORCE); 264 | } 265 | -------------------------------------------------------------------------------- /qserver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * qserver functions and definitions 6 | * Copyright 2004 Ludwig Nussel 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | #ifndef QSTAT_QSERVER_H 11 | #define QSTAT_QSERVER_H 12 | 13 | struct query_param { 14 | char *key; 15 | char *value; 16 | int i_value; 17 | unsigned int ui_value; 18 | struct query_param *next; 19 | }; 20 | 21 | typedef struct SavedData { 22 | char *data; 23 | int datalen; 24 | int pkt_index; 25 | int pkt_max; 26 | unsigned int pkt_id; 27 | struct SavedData *next; 28 | } SavedData; 29 | 30 | typedef enum { 31 | STATE_INIT = 0, 32 | STATE_CONNECTING = 1, 33 | STATE_CONNECTED = 2, 34 | STATE_QUERYING = 3, 35 | STATE_QUERIED = 4, 36 | 37 | STATE_TIMEOUT = -1, 38 | STATE_SYS_ERROR = -2, 39 | } server_state_t; 40 | 41 | struct qserver { 42 | char *arg; 43 | char *host_name; 44 | unsigned int ipaddr; 45 | int flags; 46 | server_type *type; 47 | int fd; 48 | server_state_t state; 49 | char *outfilename; 50 | char *query_arg; 51 | char *challenge_string; 52 | struct query_param *params; 53 | unsigned long challenge; 54 | unsigned short port; 55 | unsigned short orig_port; // This port is always constant from creation where as port can be updated based on the query results 56 | unsigned short query_port; 57 | 58 | // State variable to flag if the current processing call is a call from combine_packets 59 | // This should really by done via a flag to the method itself but that would require changes 60 | // to all handlers :( 61 | unsigned short combined; 62 | 63 | /** \brief number of retries _left_ for status query or rule query. 64 | * 65 | * That means 66 | * if s->retry1 == (global)n_retries then no retries were necessary so far. 67 | * if s->retry1 == 0 then the server has to be cleaned up after timeout */ 68 | int retry1; 69 | /** \brief number retries _left_ for player query. @see retry1 */ 70 | int retry2; 71 | /** \brief how much retry packets were sent */ 72 | int n_retries; 73 | /** \brief time when the last packet to the server was sent */ 74 | struct timeval packet_time1; 75 | struct timeval packet_time2; 76 | 77 | /** \brief sum of packet deltas 78 | * 79 | * average server ping is ping_total / n_requests 80 | */ 81 | int ping_total; 82 | 83 | /** \brief number of requests send to a server. 84 | * 85 | * used for ping calculation 86 | * @see ping_total 87 | */ 88 | int n_requests; 89 | 90 | /** \brief number of packets already sent to this server 91 | * 92 | * doesn't seemt to have any use 93 | */ 94 | int n_packets; 95 | 96 | /** \brief number of servers in master_pkt 97 | * 98 | * normally master_pkt_len/6 99 | */ 100 | int n_servers; 101 | /** \brief length of master_pkt */ 102 | int master_pkt_len; 103 | 104 | /** \brief IPs received from master. 105 | * 106 | * array of four bytes ip, two bytes port in network byte order 107 | */ 108 | char *master_pkt; 109 | 110 | /** \brief state info 111 | * 112 | * used for progressive master 4 bytes for WON 22 for Steam 113 | */ 114 | char master_query_tag[22]; 115 | char *error; 116 | 117 | /** \brief in-game name of the server. 118 | * 119 | * A server that has a NULL name did not receive any packets yet and is 120 | * considered down after a timeout. 121 | */ 122 | char *server_name; 123 | char *address; 124 | char *map_name; 125 | char *game; 126 | int max_players; 127 | int num_players; 128 | int protocol_version; 129 | int max_spectators; 130 | int num_spectators; 131 | 132 | SavedData saved_data; 133 | 134 | /** \brief number of the next player to retrieve info for. 135 | * 136 | * Only meaningful for servers that have type->player_packet. 137 | * This is used by q1 as it sends packets for each player individually. 138 | * cleanup_qserver() cleans up a server if next_rule == NULL and 139 | * next_player_info >= num_players 140 | */ 141 | int next_player_info; 142 | /** \brief number of player info packets received */ 143 | int n_player_info; 144 | struct player *players; 145 | 146 | /** \brief name of next rule to retreive 147 | * 148 | * Used by Q1 as it needs to send a packet for each rule. Other games would 149 | * set this to an empty string to indicate that rules need to be retrieved. 150 | * After rule packet is received set this to NULL. 151 | */ 152 | char *next_rule; 153 | int n_rules; 154 | struct rule *rules; 155 | struct rule **last_rule; 156 | int missing_rules; 157 | 158 | struct qserver *next; 159 | struct qserver *prev; 160 | }; 161 | 162 | void qserver_disconnect(struct qserver *server); 163 | 164 | /* server specific query parameters */ 165 | 166 | void add_query_param(struct qserver *server, char *arg); 167 | 168 | /** \brief get a parameter for the server as string 169 | * 170 | * @param server the server to get the value from 171 | * @param key which key to get 172 | * @param default_value value to return if key was not found 173 | * @return value for key or default_value 174 | */ 175 | char *get_param_value(struct qserver *server, const char *key, char *default_value); 176 | 177 | /** @see get_param_value */ 178 | int get_param_i_value(struct qserver *server, char *key, int default_value); 179 | 180 | /** @see get_param_value */ 181 | unsigned int get_param_ui_value(struct qserver *server, char *key, 182 | unsigned int default_value); 183 | 184 | /** \brief send an initial query packet to a server 185 | * 186 | * Sends a unicast packet to the server's file descriptor. The descriptor must 187 | * be connected. Updates n_requests or n_retries, retry1, n_packets, packet_time1 188 | * 189 | * \param server the server 190 | * \param data data to send 191 | * \param len length of data 192 | * \returns number of bytes sent or SOCKET_ERROR 193 | */ 194 | query_status_t qserver_send_initial(struct qserver *server, const char *data, size_t len); 195 | 196 | /** \brief send an initial query packet to a server 197 | * 198 | * Sends a unicast packet to the server's file descriptor. The descriptor must 199 | * be connected. Updates n_requests, n_packets, packet_time1. Sets retry1 to 200 | * (global) n_retries 201 | * 202 | * @see qserver_send_initial 203 | */ 204 | query_status_t qserver_send(struct qserver *server, const char *data, size_t len); 205 | 206 | int send_broadcast(struct qserver *server, const char *pkt, size_t pktlen); 207 | 208 | /** 209 | * Registers the send of a request packet. 210 | * 211 | * This updates n_requests, n_packets, packet_time1 and decrements n_retries 212 | */ 213 | int register_send(struct qserver *server); 214 | 215 | /** 216 | * Sends a packet to the server either direct or via broadcast. 217 | * 218 | * Once sent calls register_send to register the send of the packet 219 | */ 220 | query_status_t send_packet(struct qserver *server, const char *data, size_t len); 221 | 222 | /** 223 | * Sends a packet to the server either direct or via broadcast. 224 | */ 225 | query_status_t send_packet_raw(struct qserver *server, const char *data, size_t len); 226 | 227 | /** 228 | * Logs the error from a socket send 229 | */ 230 | query_status_t send_error(struct qserver *server, int rc); 231 | 232 | #endif 233 | -------------------------------------------------------------------------------- /info/GhostRecon.txt: -------------------------------------------------------------------------------- 1 | Ghost Recon - QStat notes 2 | ------------------------- 3 | 4 | The following Server Stats are pulled from the Ghost Recon Server - NOTE 5 | many other stats continue to work as normal due to the base qstat program. 6 | 7 | $SERVERNAME 8 | 9 | The name of the GR Server. 10 | 11 | $PLAYERS 12 | 13 | The number of Players that are playing, oberving or in the Lobby 14 | (note the ignoreserverplayer Argument above) 15 | 16 | $MAXPLAYERS 17 | 18 | The maximum players that the server will allow playing, oberving 19 | or in the Lobby (note the ignoreserverplayer Argument above) 20 | 21 | $MAP 22 | 23 | The Name of the MAP that is being used (NOTE not the Mission) 24 | 25 | $GAME 26 | 27 | The Mods that the server is running. Ex: mp1; is the Desert 28 | Seige Mod 29 | 30 | $(RULE:error) 31 | 32 | If an error occured there may be some detail here. IF the problm 33 | occurred very early in the interpretation then $SERVERNAME will 34 | hold the details. 35 | 36 | $(RULE:mission) 37 | 38 | The name of the Mission that the server is running. 39 | 40 | $(RULE:gamemode) 41 | 42 | What is the Game Mode that the server is in. Known values are 43 | COOP, TEAM and SOLO 44 | 45 | $(RULE:missiontype) 46 | 47 | What is the Mission Type. Known Values are: Mission, Firefight, 48 | Recon, Hamburger Hill, Last Man Standing, Sharpshooter, Search 49 | And Rescue, Domination, and Seige. 50 | 51 | $(RULE:dedicated) 52 | 53 | Is this server Dedicated; Yes or No. 54 | 55 | $(RULE:status) 56 | 57 | What is the Playing Status of the Server, values are Playing, 58 | Joining or Debrief. 59 | 60 | $(RULE:gametime) 61 | 62 | What is the Time limit for the Game. Values are 00:00, 05:00, 63 | 10:00, 15:00 20:00, 25:00, 30:00, 45:00 and 60:00. The 00:00 64 | is for an unlimited game. The format of this uses the -ts, 65 | -tc and -tsw command line options. 66 | 67 | $(RULE:timeplayed) 68 | 69 | How long has this game been playing. The format of this uses 70 | the -ts, -tc and -tsw command line options. 71 | 72 | $(RULE:remainingtime) 73 | 74 | How much time is left in this game. The format of this uses 75 | the -ts, -tc and -tsw command line options. 76 | 77 | $(RULE:version) 78 | 79 | What is the Version number reported by the server. Patch 1.2 = 80 | 10.1010A, Patch 1.3 = 11.101A 81 | 82 | $(RULE:spawntype) 83 | 84 | What type of spawn is in use. Known Values are None, Infinite, 85 | Individual and Team. 86 | 87 | $(RULE:spawncount) 88 | 89 | How many spawns are allowed. Enhancment possibility to add 90 | $(IF:SPAWN) to filter out when spawntype is none. 91 | 92 | $(RULE:restrict) 93 | 94 | What Weapon restrictions are in force for the server. 95 | 96 | $(RULE:password) 97 | 98 | Does the Server have a join password defined Yes or No. 99 | 100 | $(RULE:ti) 101 | 102 | Is the server using the Threat Indicator. 103 | 104 | $(RULE:motd) 105 | 106 | What is the Message Of The Day - Note these can be quite big. 107 | 108 | $(RULE:patch) 109 | 110 | What is the patch level of the GR Server. 111 | 112 | $(RULE:usestarttime) 113 | 114 | Is the server configured to start a game after "starttimeset" 115 | (Yes) OR does everyone need to click on ready (no). 116 | 117 | $(RULE:starttimeset) 118 | 119 | What time is configured to automatically start the next round. 120 | 121 | $(RULE:debrieftime) 122 | 123 | How long does the server wait at the Debrief screen after 124 | a mission. 125 | 126 | $(RULE:respawnmin) 127 | 128 | How long must a dead player wait before he can repawn. 129 | 130 | $(RULE:respawnmax) 131 | 132 | What is the longest time that a user has to respawn. 133 | 134 | $(RULE:respawnsafe) 135 | 136 | How long after respawn is a player invulnerable/cannot damage 137 | others. 138 | 139 | $(RULE:allowobservers) 140 | 141 | Does the server allow observers? Yes or No 142 | 143 | $(RULE:startwait) 144 | 145 | How long untill the automatic start timer forces the next game 146 | to start. 147 | 148 | $(RULE:iff) 149 | 150 | What Identification - Friend or Foe is configured. None, 151 | Reticule or Names 152 | 153 | $PLAYERNAME 154 | 155 | What is the Players Name. 156 | 157 | $TEAMNUM 158 | 159 | What Team Number is the Player On. Known Values are 1,2,3,4,5. 160 | 1 is Team BLUE, 2 is Team Read, 3 is Team Yellow, 4 is 161 | Team Green, 5 is Unassigned (observer or in lobby) 162 | 163 | $TEAMNAME 164 | 165 | What is the Name of the Team, see above. 166 | 167 | $DEATHS 168 | 169 | What is the health of this player. 0 Alive, 1 Dead. Note if the 170 | player has spawns remaining this can change from 1 back to 0. 171 | Enhancement possibility to add $HEALTH or $(RULE:health). 172 | Hopefully RSE/UBI will add the Deaths, Frags, and Ping to the 173 | availible information. If this happens then it would be better 174 | to have a $HEALTH 175 | 176 | $(IF:DEATHS) and $(IFNOT:DEATHS) 177 | 178 | A Test to see if the player is dead. Usefull in this constuct: 179 | $(IF:DEATHS)Dead$(ENDIF)$(IFNOT:DEATHS)Alive$(ENDIF) 180 | 181 | Ghost Recon communicates on two UDP ports and one TCP stream. Normally TCP 182 | is on port 2346 and carries the game dialog. This is the port number 183 | that is mentioned in the game so we use it and apply an offset to get the 184 | port number for status queries. Port 2347 gives some high level server stats 185 | and 2348 gives fairly low level server stats. QStat is designed around 186 | a single port per server so the 2348 port is used. One down side to this 187 | is the lack of many meaningful detail player stats (Deaths, frags, hit 188 | percentage, ping etc.). I imagines that some of these are availible in 189 | the TCP stream but that would be difficult to add to a program like QStat. 190 | 191 | The Ghost Recon packets are variable structures with a lot of string 192 | lengths. This requires fairly defensive programming as Red Storm 193 | Entertainment is not forthcoming with any details. 194 | 195 | This release adds support for the GhostRecon game. Number one note 196 | is that Red Storm and UBI do not provide the information that many 197 | Quake based users expect. Specifically they do not make Frags, Deaths 198 | Connect Time or Pings availible - at least not as far as I can tell. 199 | That said there are quite a few things that are availible and allow a 200 | server administrator to make the status of his or her server available 201 | to the public via the web. 202 | 203 | This change uses all undocumented interfaces to the Ghost Recon server 204 | so will most likely break when you install a patch. It has been tested 205 | against the Desert Seige update and several public servers. It should 206 | work against the 1.2, 1.3, and 1.4 patches and Island Thunder add-on to 207 | Ghost Recon. 208 | 209 | The Ghost Recon game type is GRS. For command-line queries, use -grs 210 | 211 | There is one query argument to this server, ignoreserverplayer. 212 | This option controls whether the first player is ignored. Ghost Recon 213 | requires that the dedicated server program take up one of the player slots 214 | (always the first slot). The ignoreserverplayer option defaults to 'yes', 215 | so the "server player" will normally not be seen. If you are running 216 | a non-dedicated server, then set ignoreserverplayer to 'no' like this: 217 | 218 | -grs,ignoreserverplayer=no 219 | 220 | Otherwise you would not be able to display your own stats. 221 | 222 | 223 | Ghost Recon support provided by Bob Marriott. 224 | -------------------------------------------------------------------------------- /packet_manip.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Packet module 6 | * Copyright 2005 Steven Hartland based on code by Steve Jankowski 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | */ 10 | 11 | #include "qstat.h" 12 | #include "debug.h" 13 | 14 | #include 15 | #include 16 | 17 | #define MAX_PACKETS 8 18 | #define MAX_FAGMENTS 128 19 | 20 | int pkt_seq = 0; 21 | int pkt_id_index = -1; 22 | int n_ids; 23 | int counts[MAX_PACKETS]; 24 | SavedData *segments[MAX_PACKETS][MAX_FAGMENTS]; 25 | 26 | int 27 | combine_packets(struct qserver *server) 28 | { 29 | unsigned int ids[MAX_PACKETS]; 30 | int maxes[MAX_PACKETS]; 31 | int lengths[MAX_PACKETS]; 32 | SavedData *sdata = &server->saved_data; 33 | int i, p, ret = INPROGRESS; 34 | 35 | n_ids = 0; 36 | 37 | memset(&segments[0][0], 0, sizeof(segments)); 38 | memset(&counts[0], 0, sizeof(counts)); 39 | memset(&lengths[0], 0, sizeof(lengths)); 40 | 41 | // foreach packet 42 | for ( ; sdata != NULL; sdata = sdata->next) { 43 | debug(4, "max:%d, id:%d\n", sdata->pkt_max, sdata->pkt_id); 44 | if (sdata->pkt_max == 0) { 45 | // not expecting multi packets or already processed? 46 | continue; 47 | } 48 | 49 | if (sdata->pkt_index >= MAX_FAGMENTS) { 50 | // we only deal up to MAX_FAGMENTS packet fragment 51 | fprintf(stderr, "Too many fragments %d for packetid %d max %d\n", sdata->pkt_index, sdata->pkt_id, MAX_FAGMENTS); 52 | return (PKT_ERROR); 53 | } 54 | 55 | for (i = 0; i < n_ids; i++) { 56 | if (sdata->pkt_id == ids[i]) { 57 | // found this packetid 58 | break; 59 | } 60 | } 61 | 62 | if (i >= n_ids) { 63 | // packetid we havent seen yet 64 | if (n_ids >= MAX_PACKETS) { 65 | // we only deal up to MAX_PACKETS packetids 66 | fprintf(stderr, "Too many distinct packetids %d max %d\n", n_ids, MAX_PACKETS); 67 | return (PKT_ERROR); 68 | } 69 | ids[n_ids] = sdata->pkt_id; 70 | maxes[n_ids] = sdata->pkt_max; 71 | i = n_ids++; 72 | } else if (maxes[i] != sdata->pkt_max) { 73 | // max's dont match 74 | debug(4, "max mismatch %d != %d", maxes[i], sdata->pkt_max); 75 | continue; 76 | } 77 | 78 | if (segments[i][sdata->pkt_index] == NULL) { 79 | // add the packet to the list of segments 80 | segments[i][sdata->pkt_index] = sdata; 81 | counts[i]++; 82 | lengths[i] += sdata->datalen; 83 | } else { 84 | debug(2, "duplicate packet detected for id %d, index %d", sdata->pkt_id, sdata->pkt_index); 85 | } 86 | } 87 | 88 | // foreach distinct packetid 89 | for (pkt_id_index = 0; pkt_id_index < n_ids; pkt_id_index++) { 90 | char *combined; 91 | int datalen = 0; 92 | int combinedlen; 93 | 94 | if (counts[pkt_id_index] != maxes[pkt_id_index]) { 95 | // we dont have all the expected packets yet 96 | debug(4, "more expected: %d != %d\n", counts[pkt_id_index], maxes[pkt_id_index]); 97 | continue; 98 | } 99 | 100 | // combine all the segments 101 | combinedlen = lengths[pkt_id_index]; 102 | combined = (char *)malloc(combinedlen); 103 | for (p = 0; p < counts[pkt_id_index]; p++) { 104 | if (segments[pkt_id_index][p] == NULL) { 105 | debug(4, "missing segment[%d][%d]", pkt_id_index, p); 106 | // reset to be unusable 107 | pkt_id_index = -1; 108 | free(combined); 109 | return (INPROGRESS); 110 | } 111 | if (datalen + segments[pkt_id_index][p]->datalen > combinedlen) { 112 | fprintf(stderr, "Data length %d > combined length %d\n", 113 | datalen + segments[pkt_id_index][p]->datalen, combinedlen); 114 | // reset to be unusable 115 | pkt_id_index = -1; 116 | free(combined); 117 | return (MEM_ERROR); 118 | } 119 | memcpy(combined + datalen, segments[pkt_id_index][p]->data, segments[pkt_id_index][p]->datalen); 120 | datalen += segments[pkt_id_index][p]->datalen; 121 | } 122 | 123 | // prevent reprocessing? 124 | for (p = 0; p < counts[pkt_id_index]; p++) { 125 | segments[pkt_id_index][p]->pkt_max = 0; 126 | } 127 | 128 | debug(4, "callback"); 129 | if (4 <= get_debug_level()) { 130 | print_packet(server, combined, datalen); 131 | } 132 | // Call the server's packet processing method flagging as a combine call 133 | server->combined = 1; 134 | ret = ((int (*)())server->type->packet_func)(server, combined, datalen); 135 | free(combined); 136 | server->combined = 0; 137 | 138 | // Note: this is currently invalid as packet processing methods 139 | // are void not int 140 | if ((INPROGRESS != ret) || (NULL == server->saved_data.data)) { 141 | break; 142 | } 143 | } 144 | // reset to be unusable 145 | pkt_id_index = -1; 146 | 147 | return (ret); 148 | } 149 | 150 | 151 | // NOTE: 152 | // pkt_id is the packet aka response identifier 153 | // pkt_index is the index of the packet fragment 154 | int 155 | add_packet(struct qserver *server, unsigned int pkt_id, int pkt_index, int pkt_max, int datalen, char *data, int calc_max) 156 | { 157 | SavedData *sdata; 158 | 159 | // safety net for bad data 160 | if (datalen == 0) { 161 | debug(1, "Empty packet received!"); 162 | return (0); 163 | } 164 | 165 | if (server->saved_data.data == NULL) { 166 | debug(4, "first packet: %d id, %d index, %d max, %d calc_max", pkt_id, pkt_index, pkt_max, calc_max); 167 | sdata = &server->saved_data; 168 | } else { 169 | debug(4, "another packet: %d id, %d index, %d max, %d calc_max", pkt_id, pkt_index, pkt_max, calc_max); 170 | if (calc_max) { 171 | // check we have the correct max 172 | SavedData *cdata = &server->saved_data; 173 | for ( ; cdata != NULL; cdata = cdata->next) { 174 | if (cdata->pkt_max > pkt_max) { 175 | pkt_max = cdata->pkt_max; 176 | } 177 | } 178 | 179 | // ensure all the packets know about this new max 180 | for (cdata = &server->saved_data; cdata != NULL; cdata = cdata->next) { 181 | cdata->pkt_max = pkt_max; 182 | } 183 | } 184 | debug(4, "calced max = %d", pkt_max); 185 | 186 | // allocate a new packet data and prepend to the list 187 | sdata = (SavedData *)calloc(1, sizeof(SavedData)); 188 | sdata->next = server->saved_data.next; 189 | server->saved_data.next = sdata; 190 | } 191 | 192 | sdata->pkt_id = pkt_id; 193 | sdata->pkt_index = pkt_index; 194 | sdata->pkt_max = pkt_max; 195 | sdata->datalen = datalen; 196 | sdata->data = (char *)malloc(sdata->datalen); 197 | if (NULL == sdata->data) { 198 | fprintf(stderr, "Out of memory\n"); 199 | return (0); 200 | } 201 | 202 | memcpy(sdata->data, data, sdata->datalen); 203 | 204 | return (1); 205 | } 206 | 207 | 208 | int 209 | next_sequence() 210 | { 211 | return (++pkt_seq); 212 | } 213 | 214 | 215 | SavedData * 216 | get_packet_fragment(int index) 217 | { 218 | if (-1 == pkt_id_index) { 219 | fprintf(stderr, "Invalid call to get_packet_fragment"); 220 | return (NULL); 221 | } 222 | 223 | if (index > counts[pkt_id_index]) { 224 | debug(4, "Invalid index requested %d > %d", index, pkt_id_index); 225 | return (NULL); 226 | } 227 | 228 | return (segments[pkt_id_index][index]); 229 | } 230 | 231 | 232 | unsigned 233 | combined_length(struct qserver *server, int pkt_id) 234 | { 235 | SavedData *sdata = &server->saved_data; 236 | unsigned len = 0; 237 | 238 | for ( ; sdata != NULL; sdata = sdata->next) { 239 | if (pkt_id == sdata->pkt_id) { 240 | len += sdata->datalen; 241 | } 242 | } 243 | return (len); 244 | } 245 | 246 | 247 | unsigned 248 | packet_count(struct qserver *server) 249 | { 250 | SavedData *sdata = &server->saved_data; 251 | unsigned cnt = 0; 252 | 253 | if (NULL == sdata->data) { 254 | return (0); 255 | } 256 | 257 | for ( ; sdata != NULL; sdata = sdata->next) { 258 | cnt++; 259 | } 260 | return (cnt); 261 | } 262 | -------------------------------------------------------------------------------- /gs2.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * New Gamespy v2 query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #endif 16 | #include 17 | #include 18 | 19 | #include "debug.h" 20 | #include "qstat.h" 21 | #include "packet_manip.h" 22 | 23 | query_status_t 24 | send_gs2_request_packet(struct qserver *server) 25 | { 26 | // The below should work but seems to make no difference to what some 27 | // servers send 28 | if (get_player_info) { 29 | server->type->status_packet[8] = 0xff; 30 | server->type->status_packet[9] = 0xff; 31 | } else { 32 | server->type->status_packet[8] = 0x00; 33 | server->type->status_packet[9] = 0x00; 34 | } 35 | 36 | return (send_packet(server, server->type->status_packet, server->type->status_len)); 37 | } 38 | 39 | 40 | // See the following for protocol details: 41 | // http://dev.kquery.com/index.php?article=42 42 | query_status_t 43 | deal_with_gs2_packet(struct qserver *server, char *rawpkt, int pktlen) 44 | { 45 | char *ptr = rawpkt; 46 | char *end = rawpkt + pktlen; 47 | unsigned char type = 0; 48 | unsigned char no_players = 0; 49 | unsigned char total_players = 0; 50 | unsigned char no_teams = 0; 51 | unsigned char total_teams = 0; 52 | unsigned char no_headers = 0; 53 | char **headers = NULL; 54 | 55 | debug(2, "processing packet..."); 56 | 57 | if (pktlen < 15) { 58 | // invalid packet? 59 | return (PKT_ERROR); 60 | } 61 | 62 | server->n_servers++; 63 | if (server->server_name == NULL) { 64 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 65 | } else { 66 | gettimeofday(&server->packet_time1, NULL); 67 | } 68 | 69 | // Could check the header here should 70 | // match the 4 byte id sent 71 | ptr += 5; 72 | while (0 == type && ptr < end) { 73 | // server info: 74 | // name value pairs null seperated 75 | // empty name && value signifies the end of section 76 | char *var, *val; 77 | int var_len, val_len; 78 | 79 | var = ptr; 80 | var_len = strlen(var); 81 | 82 | if (ptr + var_len + 2 > end) { 83 | if (0 != var_len) { 84 | malformed_packet(server, "no rule value"); 85 | } else if (get_player_info) { 86 | malformed_packet(server, "no player headers"); 87 | } 88 | return (PKT_ERROR); 89 | } 90 | ptr += var_len + 1; 91 | 92 | val = ptr; 93 | val_len = strlen(val); 94 | ptr += val_len + 1; 95 | debug(2, "var:%s (%d)=%s (%d)\n", var, var_len, val, val_len); 96 | 97 | // Lets see what we've got 98 | if (0 == strcmp(var, "hostname")) { 99 | server->server_name = strdup(val); 100 | } else if (0 == strcmp(var, "game_id")) { 101 | server->game = strdup(val); 102 | } else if (0 == strcmp(var, "gamever")) { 103 | // format: 104 | // v1.0 105 | server->protocol_version = atoi(val + 1); 106 | add_rule(server, var, val, NO_FLAGS); 107 | } else if (0 == strcmp(var, "mapname")) { 108 | server->map_name = strdup(val); 109 | } else if (0 == strcmp(var, "map")) { 110 | // For BF2MC compatibility 111 | server->map_name = strdup(val); 112 | } else if (0 == strcmp(var, "maxplayers")) { 113 | server->max_players = atoi(val); 114 | } else if (0 == strcmp(var, "numplayers")) { 115 | server->num_players = no_players = atoi(val); 116 | } else if (0 == strcmp(var, "hostport")) { 117 | change_server_port(server, atoi(val), 0); 118 | } else if (0 == var_len) { 119 | // check for end of section 120 | type = 1; 121 | } else { 122 | add_rule(server, var, val, NO_FLAGS); 123 | } 124 | } 125 | 126 | if (1 != type) { 127 | // no more info should be player headers here as we 128 | // requested it 129 | malformed_packet(server, "no player headers"); 130 | return (PKT_ERROR); 131 | } 132 | 133 | // player info header 134 | // format: 135 | // first byte = player count 136 | // followed by null seperated header 137 | no_players = (unsigned char)*ptr; 138 | debug(2, "No Players:%d\n", no_players); 139 | ptr++; 140 | 141 | if (ptr >= end) { 142 | malformed_packet(server, "no player headers"); 143 | return (PKT_ERROR); 144 | } 145 | 146 | while (1 == type && ptr < end) { 147 | // first we have the headers null seperated 148 | char **tmpp; 149 | char *head = ptr; 150 | int head_len = strlen(head); 151 | no_headers++; 152 | tmpp = (char **)realloc(headers, no_headers * sizeof(char *)); 153 | if (NULL == tmpp) { 154 | debug(0, "Failed to realloc memory for headers\n"); 155 | if (NULL != headers) { 156 | free(headers); 157 | } 158 | return (MEM_ERROR); 159 | } 160 | 161 | headers = tmpp; 162 | headers[no_headers - 1] = head; 163 | 164 | ptr += head_len + 1; 165 | 166 | // end of headers check 167 | if (0x00 == *ptr) { 168 | type = 2; 169 | ptr++; 170 | } 171 | debug(2, "player header[%d] = '%s'", no_headers - 1, head); 172 | } 173 | 174 | if (2 != type) { 175 | // no more info should be player info here as we 176 | // requested it 177 | malformed_packet(server, "no players"); 178 | return (PKT_ERROR); 179 | } 180 | 181 | while (2 == type && ptr < end) { 182 | // now each player details 183 | // add the player 184 | if (0x00 == *ptr) { 185 | // no players 186 | if (0 != no_players) { 187 | malformed_packet(server, "no players"); 188 | return (PKT_ERROR); 189 | } 190 | } else { 191 | struct player *player = add_player(server, total_players); 192 | int i; 193 | for (i = 0; i < no_headers; i++) { 194 | char *header = headers[i]; 195 | char *val; 196 | int val_len; 197 | 198 | if (ptr >= end) { 199 | malformed_packet(server, "short player detail"); 200 | return (PKT_ERROR); 201 | } 202 | val = ptr; 203 | val_len = strlen(val); 204 | ptr += val_len + 1; 205 | 206 | // lets see what we got 207 | if (0 == strcmp(header, "player_")) { 208 | player->name = strdup(val); 209 | } else if (0 == strcmp(header, "score_")) { 210 | player->score = atoi(val); 211 | } else if (0 == strcmp(header, "deaths_")) { 212 | player->deaths = atoi(val); 213 | } else if (0 == strcmp(header, "ping_")) { 214 | player->ping = atoi(val); 215 | } else if (0 == strcmp(header, "kills_")) { 216 | player->frags = atoi(val); 217 | } else if (0 == strcmp(header, "team_")) { 218 | player->team = atoi(val); 219 | } else { 220 | int len = strlen(header); 221 | if ('_' == header[len - 1]) { 222 | header[len - 1] = '\0'; 223 | } 224 | player_add_info(player, header, val, NO_FLAGS); 225 | } 226 | 227 | debug(2, "Player[%d][%s]=%s\n", total_players, headers[i], val); 228 | } 229 | total_players++; 230 | } 231 | 232 | if (total_players > no_players) { 233 | malformed_packet(server, "to many players %d > %d", total_players, no_players); 234 | return (PKT_ERROR); 235 | } 236 | 237 | // check for end of player info 238 | if (0x00 == *ptr) { 239 | if (total_players != no_players) { 240 | malformed_packet(server, "bad number of players %d != %d", total_players, no_players); 241 | return (PKT_ERROR); 242 | } 243 | type = 3; 244 | ptr++; 245 | } 246 | } 247 | 248 | if (3 != type) { 249 | // no more info should be team info here as we 250 | // requested it 251 | malformed_packet(server, "no teams"); 252 | return (PKT_ERROR); 253 | } 254 | 255 | no_teams = (unsigned char)*ptr; 256 | ptr++; 257 | 258 | debug(2, "No teams:%d\n", no_teams); 259 | no_headers = 0; 260 | 261 | while (3 == type && ptr < end) { 262 | // first we have the headers null seperated 263 | char **tmpp; 264 | char *head = ptr; 265 | int head_len = strlen(head); 266 | no_headers++; 267 | tmpp = (char **)realloc(headers, no_headers * sizeof(char *)); 268 | if (NULL == tmpp) { 269 | debug(0, "Failed to realloc memory for headers\n"); 270 | if (NULL != headers) { 271 | free(headers); 272 | } 273 | return (MEM_ERROR); 274 | } 275 | 276 | headers = tmpp; 277 | headers[no_headers - 1] = head; 278 | 279 | ptr += head_len + 1; 280 | 281 | // end of headers check 282 | if (0x00 == *ptr) { 283 | type = 4; 284 | ptr++; 285 | } 286 | } 287 | 288 | if (4 != type) { 289 | // no more info should be team info here as we 290 | // requested it 291 | malformed_packet(server, "no teams"); 292 | return (PKT_ERROR); 293 | } 294 | 295 | while (4 == type && ptr < end) { 296 | // now each teams details 297 | int i; 298 | for (i = 0; i < no_headers; i++) { 299 | char *val; 300 | int val_len; 301 | 302 | if (ptr >= end) { 303 | malformed_packet(server, "short team detail"); 304 | return (PKT_ERROR); 305 | } 306 | val = ptr; 307 | val_len = strlen(val); 308 | ptr += val_len + 1; 309 | 310 | // lets see what we got 311 | if (0 == strcmp(headers[i], "team_t")) { 312 | // BF being stupid again teams 1 based instead of 0 313 | players_set_teamname(server, total_teams + 1, val); 314 | } 315 | debug(2, "Team[%d][%s]=%s\n", total_teams, headers[i], val); 316 | } 317 | total_teams++; 318 | 319 | if (total_teams > no_teams) { 320 | malformed_packet(server, "to many teams"); 321 | return (PKT_ERROR); 322 | } 323 | } 324 | 325 | return (DONE_FORCE); 326 | } 327 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2000-2006, The Perl Foundation. 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | -------------------------------------------------------------------------------- /tm.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Gamespy query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #endif 17 | #include 18 | #include 19 | #include 20 | 21 | #include "debug.h" 22 | #include "qstat.h" 23 | #include "packet_manip.h" 24 | 25 | #define TM_XML_PREFIX "\n\nsystem.multicall\n\n" 26 | #define TM_XML_SUFFIX "\n" 27 | #define TM_SERVERINFO "methodNameGetServerOptionsparams\nmethodNameGetCurrentChallengeInfoparams\n" 28 | #define TM_PLAYERLIST "methodNameGetPlayerListparams1000\n" 29 | #define TM_AUTH_TEMPLATE "\nmethodNameAuthenticate\nparams\n%s\n%s\n" 30 | 31 | query_status_t 32 | send_tm_request_packet(struct qserver *server) 33 | { 34 | char buf[2048]; 35 | char *xmlp = buf + 8; 36 | unsigned int len; 37 | char *user = get_param_value(server, "user", NULL); 38 | char *password = get_param_value(server, "password", NULL); 39 | 40 | if (!server->protocol_version) { 41 | // No seen the version yet wait 42 | // register_send here to ensure that timeouts function correctly 43 | return (register_send(server)); 44 | } 45 | 46 | // build the query xml 47 | len = sprintf(xmlp, TM_XML_PREFIX); 48 | 49 | if ((user != NULL) && (password != NULL)) { 50 | len += sprintf(xmlp + len, TM_AUTH_TEMPLATE, user, password); 51 | } else { 52 | // Default to User / User 53 | len += sprintf(xmlp + len, TM_AUTH_TEMPLATE, "User", "User"); 54 | } 55 | 56 | // Always get Player info otherwise player count is invalid 57 | // TODO: add more calls to get full player info? 58 | server->flags |= TF_PLAYER_QUERY | TF_RULES_QUERY; 59 | len += sprintf(xmlp + len, TM_SERVERINFO); 60 | len += sprintf(xmlp + len, TM_PLAYERLIST); 61 | len += sprintf(xmlp + len, TM_XML_SUFFIX); 62 | 63 | // First 4 bytes is the length of the request 64 | memcpy(buf, &len, 4); 65 | // Second 4 bytes is the handle identifier ( id ) 66 | memcpy(buf + 4, &server->challenge, 4); 67 | 68 | // prep the details we need for multi packet responses 69 | // we expect at least 1 packet response 70 | server->saved_data.pkt_max = 1; 71 | 72 | return (send_packet(server, buf, len + 8)); 73 | } 74 | 75 | 76 | query_status_t 77 | deal_with_tm_packet(struct qserver *server, char *rawpkt, int pktlen) 78 | { 79 | char *s; 80 | char *pkt = rawpkt; 81 | char *key = NULL, *value = NULL, *tmpp = NULL; 82 | char fullname[256]; 83 | struct player *player = NULL; 84 | int pkt_max = server->saved_data.pkt_max; 85 | unsigned total_len, expected_len; 86 | int method_response = 1; 87 | 88 | debug(2, "processing..."); 89 | 90 | s = rawpkt; 91 | 92 | // We may get the setup handle and the protocol version in one packet we may not 93 | // So we continue to parse if we see the handle 94 | if ((4 <= pktlen) && (0 == memcmp(pkt, "\x0b\x00\x00\x00", 4))) { 95 | // setup handle identifier 96 | // greater 2^31 = XML-RPC, less = callback 97 | server->challenge = 0x80000001; 98 | if (4 == pktlen) { 99 | return (0); 100 | } 101 | pktlen -= 4; 102 | pkt += 4; 103 | } 104 | 105 | if ((11 <= pktlen) && (1 == sscanf(pkt, "GBXRemote %d", &server->protocol_version))) { 106 | // Got protocol version send request 107 | send_tm_request_packet(server); 108 | return (0); 109 | } 110 | 111 | if ((8 <= pktlen) && (0 == memcmp(pkt + 4, &server->challenge, 4))) { 112 | // first 4 bytes = the length 113 | // Note: We use pkt_id to store the length of the expected packet 114 | // this could cause loss but very unlikely 115 | unsigned long len; 116 | memcpy(&len, rawpkt, 4); 117 | 118 | // second 4 bytes = handle identifier we sent in the request 119 | if (8 == pktlen) { 120 | // split packet 121 | // we have at least one more packet coming 122 | if (!add_packet(server, len, 0, 2, pktlen, rawpkt, 1)) { 123 | // fatal error e.g. out of memory 124 | return (-1); 125 | } 126 | return (0); 127 | } else { 128 | // ensure the length is stored 129 | server->saved_data.pkt_id = (int)len; 130 | s += 8; 131 | } 132 | } 133 | 134 | total_len = combined_length(server, server->saved_data.pkt_id); 135 | expected_len = server->saved_data.pkt_id; 136 | debug(2, "total: %d, expected: %d\n", total_len, expected_len); 137 | if (total_len < expected_len + 8) { 138 | // we dont have a complete response add the packet 139 | int last, new_max; 140 | if (total_len + pktlen >= expected_len + 8) { 141 | last = 1; 142 | new_max = pkt_max; 143 | } else { 144 | last = 0; 145 | new_max = pkt_max + 1; 146 | } 147 | 148 | if (!add_packet(server, server->saved_data.pkt_id, pkt_max - 1, new_max, pktlen, rawpkt, 1)) { 149 | // fatal error e.g. out of memory 150 | return (-1); 151 | } 152 | 153 | if (last) { 154 | // we are the last packet run combine to call us back 155 | return (combine_packets(server)); 156 | } 157 | return (0); 158 | } 159 | 160 | server->n_servers++; 161 | if (server->server_name == NULL) { 162 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 163 | } else { 164 | gettimeofday(&server->packet_time1, NULL); 165 | } 166 | 167 | // Terminate the packet data 168 | pkt = (char *)malloc(pktlen + 1); 169 | if (NULL == pkt) { 170 | debug(0, "Failed to malloc memory for packet terminator\n"); 171 | return (MEM_ERROR); 172 | } 173 | memcpy(pkt, rawpkt, pktlen); 174 | pkt[pktlen] = '\0'; 175 | 176 | //fprintf( stderr, "S=%s\n", s ); 177 | s = strtok(pkt + 8, "\015\012"); 178 | while (NULL != s) { 179 | //fprintf( stderr, "S=%s\n", s ); 180 | if (0 == strncmp(s, "", 14)) { 181 | key = s + 14; 182 | tmpp = strstr(key, ""); 183 | if (NULL != tmpp) { 184 | *tmpp = '\0'; 185 | } 186 | s = strtok(NULL, "\015\012"); 187 | value = NULL; 188 | continue; 189 | } else if ((NULL != key) && (0 == strncmp(s, "", 7))) { 190 | // value 191 | s += 7; 192 | if (0 == strncmp(s, "", 8)) { 193 | // String 194 | value = s + 8; 195 | tmpp = strstr(s, ""); 196 | } else if (0 == strncmp(s, "", 4)) { 197 | // Int 198 | value = s + 4; 199 | tmpp = strstr(s, ""); 200 | } else if (0 == strncmp(s, "", 9)) { 201 | // Boolean 202 | value = s + 9; 203 | tmpp = strstr(s, ""); 204 | } else if (0 == strncmp(s, "", 8)) { 205 | // Double 206 | value = s + 8; 207 | tmpp = strstr(s, ""); 208 | } 209 | // also have struct and array but not interested in those 210 | 211 | if (NULL != tmpp) { 212 | *tmpp = '\0'; 213 | } 214 | 215 | if (NULL != value) { 216 | debug(4, "%s = %s\n", key, value); 217 | } 218 | } else if ((0 == strncmp(s, "", 9)) && (3 > method_response)) { 219 | // end of method response 220 | method_response++; 221 | } 222 | 223 | if ((NULL != value) && (NULL != key)) { 224 | switch (method_response) { 225 | case 1: 226 | // GetServerOptions response 227 | if (0 == strcmp("Name", key)) { 228 | server->server_name = strdup(value); 229 | } else if (0 == strcmp("CurrentMaxPlayers", key)) { 230 | server->max_players = atoi(value); 231 | } else { 232 | sprintf(fullname, "server.%s", key); 233 | add_rule(server, fullname, value, NO_FLAGS); 234 | } 235 | break; 236 | 237 | case 2: 238 | // GetCurrentChallengeInfo response 239 | if (0 == strcmp("Name", key)) { 240 | server->map_name = strdup(value); 241 | } else { 242 | sprintf(fullname, "challenge.%s", key); 243 | add_rule(server, fullname, value, NO_FLAGS); 244 | } 245 | break; 246 | 247 | case 3: 248 | // GetPlayerList response 249 | // Player info 250 | if (0 == strcmp("Login", key)) { 251 | player = add_player(server, server->n_player_info); 252 | server->num_players++; 253 | } else if (NULL != player) { 254 | if (0 == strcmp("NickName", key)) { 255 | player->name = strdup(value); 256 | } else if (0 == strcmp("PlayerId", key)) { 257 | //player->number = atoi( value ); 258 | } else if (0 == strcmp("TeamId", key)) { 259 | player->team = atoi(value); 260 | } else if (0 == strcmp("IsSpectator", key)) { 261 | player->flags = player->flags & 1; 262 | } else if (0 == strcmp("IsInOfficialMode", key)) { 263 | player->flags = player->flags & 2; 264 | } else if (0 == strcmp("LadderRanking", key)) { 265 | player->score = atoi(value); 266 | } 267 | } 268 | break; 269 | } 270 | value = NULL; 271 | } 272 | s = strtok(NULL, "\015\012"); 273 | } 274 | 275 | free(pkt); 276 | 277 | if (0 == strncmp(rawpkt + pktlen - 19, "", 17)) { 278 | // last packet seen 279 | return (DONE_FORCE); 280 | } 281 | 282 | return (INPROGRESS); 283 | } 284 | -------------------------------------------------------------------------------- /ottd.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * 4 | * opentTTD protocol 5 | * Copyright 2007 Ludwig Nussel 6 | * 7 | * Licensed under the Artistic License, see LICENSE.txt for license terms 8 | */ 9 | 10 | #include 11 | #ifndef _WIN32 12 | #include 13 | #endif 14 | #include 15 | #include 16 | 17 | #include "qstat.h" 18 | #include "qserver.h" 19 | #include "debug.h" 20 | 21 | enum { 22 | MAX_VEHICLE_TYPES = 5, MAX_STATION_TYPES = 5 23 | }; 24 | 25 | static const char *vehicle_types[] = 26 | { 27 | "num_trains", 28 | "num_trucks", 29 | "num_busses", 30 | "num_aircrafts", 31 | "num_ships", 32 | }; 33 | 34 | static const char *station_types[] = 35 | { 36 | "num_stations", 37 | "num_truckbays", 38 | "num_busstations", 39 | "num_airports", 40 | "num_docks", 41 | }; 42 | 43 | query_status_t 44 | deal_with_ottdmaster_packet(struct qserver *server, char *rawpkt, int pktlen) 45 | { 46 | unsigned num; 47 | 48 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 49 | server->server_name = MASTER; 50 | 51 | if (swap_short_from_little(rawpkt) != pktlen) { 52 | malformed_packet(server, "invalid packet length"); 53 | return (PKT_ERROR); 54 | } 55 | if (rawpkt[2] != 7) { 56 | malformed_packet(server, "invalid packet type"); 57 | return (PKT_ERROR); 58 | } 59 | 60 | if (rawpkt[3] != 1) { 61 | malformed_packet(server, "invalid packet version"); 62 | return (PKT_ERROR); 63 | } 64 | 65 | num = swap_short_from_little(&rawpkt[4]); 66 | rawpkt += 6; 67 | pktlen -= 6; 68 | if (num && (num * 6 <= pktlen)) { 69 | unsigned i; 70 | server->master_pkt = (char *)realloc(server->master_pkt, server->master_pkt_len + pktlen); 71 | memset(server->master_pkt + server->master_pkt_len, 0, pktlen); 72 | server->master_pkt_len += pktlen; 73 | for (i = 0; i < num * 6; i += 6) { 74 | memcpy(&server->master_pkt[i], &rawpkt[i], 4); 75 | server->master_pkt[i + 4] = rawpkt[i + 5]; 76 | server->master_pkt[i + 5] = rawpkt[i + 4]; 77 | } 78 | server->n_servers += num; 79 | } else { 80 | malformed_packet(server, "invalid packet"); 81 | return (PKT_ERROR); 82 | } 83 | 84 | bind_sockets(); 85 | 86 | return (DONE_AUTO); 87 | } 88 | 89 | 90 | #define xstr(s) str(s) 91 | #define str(s) # s 92 | 93 | #define GET_STRING \ 94 | do { \ 95 | str = (char *)ptr; \ 96 | ptr = memchr(ptr, '\0', end - ptr); \ 97 | if (!ptr) \ 98 | { \ 99 | malformed_packet(server, "%s:%s invalid packet", __FILE__, xstr(__LINE__)); \ 100 | return PKT_ERROR; \ 101 | } \ 102 | ++ptr; \ 103 | } while (0) 104 | 105 | #define FAIL_IF(cond, msg) \ 106 | if ((cond)) { \ 107 | malformed_packet(server, "%s:%s %s", __FILE__, xstr(__LINE__), msg); \ 108 | return PKT_ERROR; \ 109 | } 110 | 111 | #define INVALID_IF(cond) \ 112 | FAIL_IF(cond, "invalid packet") 113 | 114 | query_status_t 115 | deal_with_ottd_packet(struct qserver *server, char *rawpkt, int pktlen) 116 | { 117 | unsigned char *ptr = (unsigned char *)rawpkt; 118 | unsigned char *end = (unsigned char *)(rawpkt + pktlen); 119 | unsigned char type; 120 | char *str; 121 | char buf[32]; 122 | unsigned ver; 123 | 124 | server->n_servers++; 125 | if (server->server_name == NULL) { 126 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 127 | server->n_requests++; 128 | } else { 129 | gettimeofday(&server->packet_time1, NULL); 130 | } 131 | 132 | FAIL_IF(pktlen < 4 || swap_short_from_little(rawpkt) > pktlen, "invalid packet"); 133 | 134 | type = ptr[2]; 135 | ver = ptr[3]; 136 | ptr += 4; 137 | 138 | debug(3, "len %hu type %hhu ver %u", swap_short_from_little(rawpkt), type, ver); 139 | 140 | FAIL_IF(ver != 4 && ver != 5, "only version 4 and 5 servers are supported"); 141 | 142 | if (type == 1) {// info packet 143 | unsigned numgrf = *ptr; 144 | FAIL_IF(ptr + numgrf * 20 + 1 > end, "invalid newgrf number"); 145 | ptr += numgrf * 20 + 1; 146 | 147 | snprintf(buf, sizeof(buf), "%u", swap_long_from_little(ptr)); 148 | add_rule(server, "date_days", buf, NO_FLAGS); 149 | ptr += 4; 150 | 151 | snprintf(buf, sizeof(buf), "%u", swap_long_from_little(ptr)); 152 | add_rule(server, "startdate_days", buf, NO_FLAGS); 153 | ptr += 4; 154 | 155 | FAIL_IF(ptr + 3 > end, "invalid packet"); 156 | 157 | snprintf(buf, sizeof(buf), "%hhu", ptr[0]); 158 | add_rule(server, "maxcompanies", buf, NO_FLAGS); 159 | snprintf(buf, sizeof(buf), "%hhu", ptr[1]); 160 | add_rule(server, "numcompanies", buf, NO_FLAGS); 161 | server->max_spectators = ptr[2]; 162 | ptr += 3; 163 | 164 | GET_STRING; 165 | server->server_name = strdup(str); 166 | 167 | GET_STRING; 168 | add_rule(server, "version", str, NO_FLAGS); 169 | 170 | FAIL_IF(ptr + 7 > end, "invalid packet"); 171 | 172 | { 173 | static const char *langs[] = 174 | { 175 | "any", 176 | "English", 177 | "German", 178 | "French" 179 | }; 180 | unsigned i = *ptr++; 181 | if (i > 3) { 182 | i = 0; 183 | } 184 | add_rule(server, "language", (char *)langs[i], NO_FLAGS); 185 | } 186 | 187 | add_rule(server, "password", *ptr++ ? "1" : "0", NO_FLAGS); 188 | 189 | server->max_players = *ptr++; 190 | server->num_players = *ptr++; 191 | server->num_spectators = *ptr++; 192 | 193 | GET_STRING; 194 | 195 | server->map_name = strdup(str); 196 | 197 | snprintf(buf, sizeof(buf), "%hu", swap_short_from_little(ptr)); 198 | add_rule(server, "map_width", buf, NO_FLAGS); 199 | snprintf(buf, sizeof(buf), "%hu", swap_short_from_little(ptr)); 200 | add_rule(server, "map_height", buf, NO_FLAGS); 201 | 202 | { 203 | static const char *sets[] = 204 | { 205 | "temperate", 206 | "arctic", 207 | "desert", 208 | "toyland" 209 | }; 210 | unsigned i = *ptr++; 211 | if (i > 3) { 212 | i = 0; 213 | } 214 | add_rule(server, "map_set", (char *)sets[i], NO_FLAGS); 215 | } 216 | 217 | add_rule(server, "dedicated", *ptr++ ? "1" : "0", NO_FLAGS); 218 | } else if (type == 3) { // player packet 219 | unsigned i, j; 220 | INVALID_IF(ptr + 2 > end); 221 | 222 | server->num_players = *ptr++; 223 | 224 | for (i = 0; i < server->num_players; ++i) { 225 | unsigned long long lli; 226 | struct player *player; 227 | unsigned char nr; 228 | 229 | nr = *ptr++; 230 | 231 | debug(3, "player number %d", nr); 232 | player = add_player(server, i); 233 | FAIL_IF(!player, "can't allocate player"); 234 | 235 | GET_STRING; 236 | player->name = strdup(str); 237 | debug(3, "name %s", str); 238 | player->frags = 0; 239 | 240 | INVALID_IF(ptr + 4 + 3 * 8 + 2 + 1 + 2 * MAX_VEHICLE_TYPES + 2 * MAX_STATION_TYPES > end); 241 | 242 | snprintf(buf, sizeof(buf), "%u", swap_long_from_little(ptr)); 243 | player_add_info(player, "startdate", buf, 0); 244 | ptr += 4; 245 | 246 | lli = swap_long_from_little(ptr + 4); 247 | lli <<= 32; 248 | lli += swap_long_from_little(ptr); 249 | snprintf(buf, sizeof(buf), "%lld", lli); 250 | player_add_info(player, "value", buf, 0); 251 | ptr += 8; 252 | 253 | lli = swap_long_from_little(ptr + 4); 254 | lli <<= 32; 255 | lli = swap_long_from_little(ptr); 256 | snprintf(buf, sizeof(buf), "%lld", lli); 257 | player_add_info(player, "money", buf, 0); 258 | ptr += 8; 259 | 260 | lli = swap_long_from_little(ptr + 4); 261 | lli <<= 32; 262 | lli += swap_long_from_little(ptr); 263 | snprintf(buf, sizeof(buf), "%lld", lli); 264 | player_add_info(player, "income", buf, 0); 265 | ptr += 8; 266 | 267 | snprintf(buf, sizeof(buf), "%hu", swap_short_from_little(ptr)); 268 | player_add_info(player, "performance", buf, 0); 269 | ptr += 2; 270 | 271 | player_add_info(player, "password", *ptr ? "1" : "0", 0); 272 | ++ptr; 273 | 274 | for (j = 0; j < MAX_VEHICLE_TYPES; ++j) { 275 | snprintf(buf, sizeof(buf), "%hu", swap_short_from_little(ptr)); 276 | player_add_info(player, (char *)vehicle_types[j], buf, 0); 277 | ptr += 2; 278 | } 279 | for (j = 0; j < MAX_STATION_TYPES; ++j) { 280 | snprintf(buf, sizeof(buf), "%hu", swap_short_from_little(ptr)); 281 | player_add_info(player, (char *)station_types[j], buf, 0); 282 | ptr += 2; 283 | } 284 | 285 | if (ver != 5) { 286 | // connections 287 | while (ptr + 1 < end && *ptr) { 288 | ++ptr; 289 | GET_STRING; // client name 290 | debug(3, "%s played by %s", str, player->name); 291 | GET_STRING; // id 292 | INVALID_IF(ptr + 4 > end); 293 | ptr += 4; 294 | } 295 | 296 | ++ptr; // record terminated by zero byte 297 | } 298 | } 299 | 300 | // spectators 301 | while (ptr + 1 < end && *ptr) { 302 | ++ptr; 303 | GET_STRING; // client name 304 | debug(3, "spectator %s", str); 305 | GET_STRING; // id 306 | INVALID_IF(ptr + 4 > end); 307 | ptr += 4; 308 | } 309 | ++ptr; // record terminated by zero byte 310 | 311 | server->next_rule = NO_SERVER_RULES; // we're done 312 | server->next_player_info = server->num_players; // we're done 313 | } else { 314 | malformed_packet(server, "invalid type"); 315 | return (PKT_ERROR); 316 | } 317 | 318 | server->retry1 = n_retries; // we're done with this packet, reset retry counter 319 | 320 | return (DONE_AUTO); 321 | } 322 | 323 | 324 | query_status_t 325 | send_ottdmaster_request_packet(struct qserver *server) 326 | { 327 | return (qserver_send_initial(server, server->type->master_packet, server->type->master_len)); 328 | } 329 | 330 | 331 | query_status_t 332 | send_ottd_request_packet(struct qserver *server) 333 | { 334 | qserver_send_initial(server, server->type->status_packet, server->type->status_len); 335 | 336 | if (get_server_rules || get_player_info) { 337 | server->next_rule = ""; // trigger calling send_a2s_rule_request_packet 338 | } 339 | 340 | return (INPROGRESS); 341 | } 342 | -------------------------------------------------------------------------------- /dirtybomb.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * DirtyBomb query protocol 6 | * Copyright 2012 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | #include "debug.h" 26 | #include "utils.h" 27 | #include "qstat.h" 28 | #include "md5.h" 29 | #include "packet_manip.h" 30 | 31 | int unpack_msgpack(struct qserver *server, unsigned char *, unsigned char *); 32 | 33 | query_status_t 34 | send_dirtybomb_request_packet(struct qserver *server) 35 | { 36 | char buf[1024], *password, chunks, len; 37 | 38 | password = get_param_value(server, "password", ""); 39 | len = strlen(password); 40 | chunks = 0x01; 41 | if (server->flags & TF_RULES_QUERY) { 42 | chunks |= 0x02; 43 | } 44 | if (server->flags & TF_PLAYER_QUERY) { 45 | chunks |= 0x04; // Player 46 | chunks |= 0x08; // Team - Currently not supported 47 | } 48 | sprintf(buf, "%c%c%s%c", 0x01, len, password, chunks); 49 | 50 | server->saved_data.pkt_max = -1; 51 | 52 | return (send_packet(server, buf, len + 3)); 53 | } 54 | 55 | 56 | query_status_t 57 | deal_with_dirtybomb_packet(struct qserver *server, char *rawpkt, int pktlen) 58 | { 59 | unsigned char *s, *l, pkt_id, pkt_max; 60 | 61 | debug(2, "processing..."); 62 | 63 | if (8 > pktlen) { 64 | // not valid response 65 | malformed_packet(server, "packet too small"); 66 | return (PKT_ERROR); 67 | } 68 | 69 | if (rawpkt[4] != 0x01) { 70 | // not a known response 71 | malformed_packet(server, "unknown packet type 0x%02hhx", rawpkt[4]); 72 | return (PKT_ERROR); 73 | } 74 | 75 | // Response ID - long 76 | // Response Type - byte 77 | // Current Packet Number - byte 78 | pkt_id = rawpkt[5]; 79 | 80 | // Last Packet Number - byte 81 | pkt_max = rawpkt[6]; 82 | 83 | // Payload Blob - msgpack map 84 | s = (unsigned char *)rawpkt + 7; 85 | 86 | if (!server->combined) { 87 | int pkt_cnt = packet_count(server); 88 | 89 | server->retry1 = n_retries; 90 | if (0 == server->n_requests) { 91 | server->ping_total = time_delta(&packet_recv_time, &server->packet_time1); 92 | server->n_requests++; 93 | } 94 | 95 | if (pkt_cnt < pkt_max) { 96 | // We're expecting more to come 97 | debug(5, "fragment recieved..."); 98 | 99 | if (!add_packet(server, 0, pkt_id, pkt_max, pktlen, rawpkt, 1)) { 100 | // fatal error e.g. out of memory 101 | return (MEM_ERROR); 102 | } 103 | 104 | // combine_packets will call us recursively 105 | return (combine_packets(server)); 106 | } 107 | } 108 | 109 | // Correct ping 110 | // Not quite right but gives a good estimate 111 | server->ping_total = (server->ping_total * server->n_requests) / 2; 112 | 113 | debug(3, "processing response..."); 114 | 115 | gettimeofday(&server->packet_time1, NULL); 116 | 117 | l = (unsigned char *)rawpkt + pktlen - 1; 118 | if (!unpack_msgpack(server, s, l)) { 119 | return (PKT_ERROR); 120 | } 121 | 122 | return (DONE_FORCE); 123 | } 124 | 125 | 126 | int 127 | unpack_msgpack_pos_fixnum(struct qserver *server, uint8_t *val, unsigned char **datap, unsigned char *last, int raw) 128 | { 129 | if (!raw) { 130 | if (*datap > last) { 131 | malformed_packet(server, "packet too small"); 132 | return (0); 133 | } 134 | 135 | if ((**datap & 0x80) != 0) { 136 | malformed_packet(server, "invalid positive fixnum type 0x%02hhx", **datap); 137 | return (0); 138 | } 139 | } 140 | 141 | *val = **datap; 142 | (*datap)++; 143 | 144 | return (1); 145 | } 146 | 147 | 148 | int 149 | unpack_msgpack_uint16(struct qserver *server, uint16_t *val, unsigned char **datap, unsigned char *last, int raw) 150 | { 151 | if (!raw) { 152 | if (*datap > last) { 153 | malformed_packet(server, "packet too small"); 154 | return (0); 155 | } 156 | 157 | if (**datap != 0xcd) { 158 | malformed_packet(server, "invalid uint16 type 0x%02hhx", **datap); 159 | return (0); 160 | } 161 | 162 | (*datap)++; 163 | } 164 | 165 | if (*datap + 2 > last) { 166 | malformed_packet(server, "packet too small"); 167 | return (0); 168 | } 169 | *val = (uint16_t)(*datap)[1] | ((uint16_t)(*datap)[0] << 8); 170 | *datap += 2; 171 | 172 | return (1); 173 | } 174 | 175 | 176 | int 177 | unpack_msgpack_uint32(struct qserver *server, uint32_t *val, unsigned char **datap, unsigned char *last, int raw) 178 | { 179 | if (!raw) { 180 | if (*datap > last) { 181 | malformed_packet(server, "packet too small"); 182 | return (0); 183 | } 184 | 185 | if (**datap != 0xce) { 186 | malformed_packet(server, "invalid uint32 type 0x%02hhx", **datap); 187 | return (0); 188 | } 189 | 190 | (*datap)++; 191 | } 192 | 193 | if (*datap + 4 > last) { 194 | malformed_packet(server, "packet too small"); 195 | return (0); 196 | } 197 | 198 | *val = (uint32_t)(*datap)[3] | 199 | ((uint32_t)(*datap)[2] << 8) | 200 | ((uint32_t)(*datap)[1] << 16) | 201 | ((uint32_t)(*datap)[0] << 24); 202 | *datap += 2; 203 | 204 | return (1); 205 | } 206 | 207 | 208 | int 209 | unpack_msgpack_raw_len(struct qserver *server, char **valp, unsigned char **datap, unsigned char *last, uint32_t len) 210 | { 211 | char *data; 212 | 213 | debug(4, "raw_len: 0x%lu\n", (unsigned long)len); 214 | 215 | if (*datap + len > last) { 216 | malformed_packet(server, "packet too small"); 217 | return (0); 218 | } 219 | 220 | if (NULL == (data = malloc(len + 1))) { 221 | malformed_packet(server, "out of memory"); 222 | return (0); 223 | } 224 | 225 | memcpy(data, *datap, len); 226 | data[len] = '\0'; 227 | 228 | *valp = data; 229 | (*datap) += len; 230 | 231 | return (1); 232 | } 233 | 234 | 235 | int 236 | unpack_msgpack_raw(struct qserver *server, char **valp, unsigned char **datap, unsigned char *last) 237 | { 238 | unsigned char type, len; 239 | 240 | if (*datap + 1 > last) { 241 | malformed_packet(server, "packet too small"); 242 | return (0); 243 | } 244 | 245 | type = (**datap & 0xa0); 246 | len = (**datap & 0x1f); 247 | if (0xa0 != type) { 248 | malformed_packet(server, "invalid raw type 0x%02hhx", **datap); 249 | return (0); 250 | } 251 | (*datap)++; 252 | 253 | return (unpack_msgpack_raw_len(server, valp, datap, last, (uint32_t)len)); 254 | } 255 | 256 | 257 | int 258 | unpack_msgpack_raw16(struct qserver *server, char **valp, unsigned char **datap, unsigned char *last) 259 | { 260 | uint16_t len; 261 | 262 | if (*datap + 3 > last) { 263 | malformed_packet(server, "packet too small"); 264 | return (0); 265 | } 266 | (*datap)++; 267 | 268 | if (!unpack_msgpack_uint16(server, &len, datap, last, 1)) { 269 | return (0); 270 | } 271 | 272 | return (unpack_msgpack_raw_len(server, valp, datap, last, (uint32_t)len)); 273 | } 274 | 275 | 276 | int 277 | unpack_msgpack_raw32(struct qserver *server, char **valp, unsigned char **datap, unsigned char *last) 278 | { 279 | uint32_t len; 280 | 281 | if (*datap + 5 > last) { 282 | malformed_packet(server, "packet too small"); 283 | return (0); 284 | } 285 | (*datap)++; 286 | 287 | if (!unpack_msgpack_uint32(server, &len, datap, last, 1)) { 288 | return (0); 289 | } 290 | 291 | return (unpack_msgpack_raw_len(server, valp, datap, last, len)); 292 | } 293 | 294 | 295 | int 296 | unpack_msgpack_string(struct qserver *server, char **valp, unsigned char **datap, unsigned char *last) 297 | { 298 | unsigned char type = **datap; 299 | 300 | debug(4, "string: 0x%02hhx\n", type); 301 | 302 | if (0xa0 == (type & 0xa0)) { 303 | return (unpack_msgpack_raw(server, valp, datap, last)); 304 | } else if (type == 0xda) { 305 | return (unpack_msgpack_raw16(server, valp, datap, last)); 306 | } else if (type == 0xdb) { 307 | return (unpack_msgpack_raw32(server, valp, datap, last)); 308 | } else { 309 | malformed_packet(server, "invalid string type 0x%02hhx", type); 310 | } 311 | 312 | return (0); 313 | } 314 | 315 | 316 | int 317 | unpack_msgpack(struct qserver *server, unsigned char *s, unsigned char *l) 318 | { 319 | uint16_t elements; 320 | unsigned char type, type_len; 321 | char *var, *str_val; 322 | uint8_t uint8_val; 323 | 324 | type_len = *s; 325 | s++; 326 | debug(3, "type/len: 0x%02hhx\n", type_len); 327 | if (0x80 == (type_len & 0x80)) { 328 | type = (type_len & 0x80); 329 | elements = (type_len & 0x0f); 330 | debug(3, "map type: 0x%02hhx, elements: %d\n", type, elements); 331 | } else if (type_len == 0xde) { 332 | // map 16 333 | if (!unpack_msgpack_uint16(server, &elements, &s, l, 1)) { 334 | return (0); 335 | } 336 | } else { 337 | // There is a map 32 but we don't support it 338 | malformed_packet(server, "invalid map type 0x%02hhx", type_len); 339 | return (0); 340 | } 341 | 342 | while (elements) { 343 | type = *s; 344 | if (!unpack_msgpack_string(server, &var, &s, l)) { 345 | return (0); 346 | } 347 | 348 | debug(4, "Map[%s]\n", var); 349 | 350 | if (0 == strcmp(var, "SN")) { 351 | // Server Name 352 | if (!unpack_msgpack_string(server, &str_val, &s, l)) { 353 | return (0); 354 | } 355 | server->server_name = str_val; 356 | } else if (0 == strcmp(var, "MAP")) { 357 | // Map 358 | if (!unpack_msgpack_string(server, &str_val, &s, l)) { 359 | return (0); 360 | } 361 | server->map_name = str_val; 362 | } else if (0 == strcmp(var, "MP")) { 363 | // Max Players 364 | if (!unpack_msgpack_pos_fixnum(server, &uint8_val, &s, l, 0)) { 365 | return (0); 366 | } 367 | server->max_players = uint8_val; 368 | } else if (0 == strcmp(var, "NP")) { 369 | // Number of Players 370 | if (!unpack_msgpack_pos_fixnum(server, &uint8_val, &s, l, 0)) { 371 | return (0); 372 | } 373 | server->num_players = uint8_val; 374 | } else if (0 == strcmp(var, "RL")) { 375 | // Gametype 376 | if (!unpack_msgpack_string(server, &str_val, &s, l)) { 377 | return (0); 378 | } 379 | 380 | server->game = str_val; 381 | add_rule(server, "gametype", str_val, NO_FLAGS); 382 | } else if (0 == strcmp(var, "SFT")) { 383 | // Current Frametime in ms 384 | if (!unpack_msgpack_pos_fixnum(server, &uint8_val, &s, l, 0)) { 385 | return (0); 386 | } 387 | } else { 388 | debug(4, "Unknown setting '%s'\n", var); 389 | s++; 390 | } 391 | 392 | free(var); 393 | elements--; 394 | } 395 | 396 | return (1); 397 | } 398 | -------------------------------------------------------------------------------- /gps.c: -------------------------------------------------------------------------------- 1 | /* 2 | * qstat 3 | * by Steve Jankowski 4 | * 5 | * Gamespy query protocol 6 | * Copyright 2005 Steven Hartland 7 | * 8 | * Licensed under the Artistic License, see LICENSE.txt for license terms 9 | * 10 | */ 11 | 12 | #include 13 | #ifndef _WIN32 14 | #include 15 | #endif 16 | #include 17 | #include 18 | #include 19 | 20 | #include "debug.h" 21 | #include "qstat.h" 22 | #include "packet_manip.h" 23 | 24 | int 25 | gps_max_players(struct qserver *server) 26 | { 27 | struct player *player; 28 | int no_players = 0; 29 | 30 | if (0 == server->num_players) { 31 | return (0); 32 | } 33 | 34 | for (player = server->players; player; player = player->next) { 35 | no_players++; 36 | } 37 | 38 | return ((no_players < server->num_players) ? 1 : 0); 39 | } 40 | 41 | 42 | int 43 | gps_player_info_key(char *s, char *end) 44 | { 45 | static char *keys[] = 46 | { 47 | "frags_", "team_", "ping_", "species_", 48 | "race_", "deaths_", "score_", "enemy_", 49 | "player_", "keyhash_", "teamname_", 50 | "playername_", "keyhash_", "kills_", "queryid" 51 | }; 52 | int i; 53 | 54 | for (i = 0; i < sizeof(keys) / sizeof(char *); i++) { 55 | int len = strlen(keys[i]); 56 | if ((s + len < end) && (strncmp(s, keys[i], len) == 0)) { 57 | return (len); 58 | } 59 | } 60 | return (0); 61 | } 62 | 63 | 64 | query_status_t 65 | send_gps_request_packet(struct qserver *server) 66 | { 67 | return (send_packet(server, server->type->status_packet, server->type->status_len)); 68 | } 69 | 70 | 71 | query_status_t 72 | deal_with_gps_packet(struct qserver *server, char *rawpkt, int pktlen) 73 | { 74 | char *s, *key, *value, *end; 75 | struct player *player = NULL; 76 | int id_major = 0, id_minor = 0, final = 0, player_num; 77 | char tmp[256]; 78 | 79 | debug(2, "processing..."); 80 | 81 | server->n_servers++; 82 | if (server->server_name == NULL) { 83 | server->ping_total += time_delta(&packet_recv_time, &server->packet_time1); 84 | } else { 85 | gettimeofday(&server->packet_time1, NULL); 86 | } 87 | 88 | /* 89 | * // We're using the saved_data a bit differently here to track received 90 | * // packets. 91 | * // pkt_id is the id_major from the \queryid\ 92 | * // pkt_max is the total number of packets expected 93 | * // pkt_index is a bit mask of the packets received. The id_minor of 94 | * // \queryid\ provides packet numbers (1 through pkt_max). 95 | */ 96 | if (server->saved_data.pkt_index == -1) { 97 | server->saved_data.pkt_index = 0; 98 | } 99 | 100 | rawpkt[pktlen] = '\0'; 101 | end = &rawpkt[pktlen]; 102 | 103 | s = rawpkt; 104 | while (*s) { 105 | // find the '\' 106 | while (*s && *s == '\\') { 107 | s++; 108 | } 109 | 110 | if (!*s) { 111 | // out of packet 112 | break; 113 | } 114 | // Start of key 115 | key = s; 116 | 117 | // while we still have data and its not a '\' 118 | while (*s && *s != '\\') { 119 | s++; 120 | } 121 | 122 | if (!*s) { 123 | // out of packet 124 | break; 125 | } 126 | 127 | // Terminate the key 128 | *s++ = '\0'; 129 | 130 | // Now for the value 131 | value = s; 132 | 133 | // while we still have data and its not a '\' 134 | while (*s && *s != '\\') { 135 | s++; 136 | } 137 | 138 | if (s[0] && s[1]) { 139 | //fprintf( stderr, "%s = %s\n", key, value ); 140 | if (!isalpha((unsigned char)s[1])) { 141 | // escape char? 142 | s++; 143 | // while we still have data and its not a '\' 144 | while (*s && *s != '\\') { 145 | s++; 146 | } 147 | } else if ( 148 | isalpha((unsigned char)s[1]) && 149 | (0 == strncmp(key, "player_", 7)) && 150 | (0 != strcmp(key, "player_flags")) 151 | ) { 152 | // possible '\' in player name 153 | if (!gps_player_info_key(s + 1, end)) { 154 | // yep there was an escape in the player name 155 | s++; 156 | // while we still have data and its not a '\' 157 | while (*s && *s != '\\') { 158 | s++; 159 | } 160 | } 161 | } 162 | } 163 | 164 | if (*s) { 165 | *s++ = '\0'; 166 | } 167 | 168 | //fprintf( stderr, "%s = %s\n", key, value ); 169 | if (*value == '\0') { 170 | if (strcmp(key, "final") == 0) { 171 | final = 1; 172 | if (id_minor > server->saved_data.pkt_max) { 173 | server->saved_data.pkt_max = id_minor; 174 | } 175 | continue; 176 | } 177 | } 178 | 179 | /* This must be done before looking for player info because 180 | * "queryid" is a valid according to gps_player_info_key(). 181 | */ 182 | if (strcmp(key, "queryid") == 0) { 183 | sscanf(value, "%d.%d", &id_major, &id_minor); 184 | if (server->saved_data.pkt_id == 0) { 185 | server->saved_data.pkt_id = id_major; 186 | } 187 | if (id_major == server->saved_data.pkt_id) { 188 | if (id_minor > 0) { 189 | // pkt_index is bitmask of packets recieved 190 | server->saved_data.pkt_index |= 1 << (id_minor - 1); 191 | } 192 | if (final && (id_minor > server->saved_data.pkt_max)) { 193 | server->saved_data.pkt_max = id_minor; 194 | } 195 | } 196 | continue; 197 | } 198 | 199 | if (player == NULL) { 200 | int len = gps_player_info_key(key, end); 201 | if (len) { 202 | // We have player info 203 | int player_number = atoi(key + len); 204 | player = get_player_by_number(server, player_number); 205 | 206 | // && gps_max_players( server ) due to bf1942 issue 207 | // where the actual no players is correct but more player 208 | // details are returned 209 | if ((player == NULL) && gps_max_players(server)) { 210 | player = add_player(server, player_number); 211 | if (player) { 212 | // init to -1 so we can tell if 213 | // we have team info 214 | player->team = -1; 215 | player->deaths = -999; 216 | } 217 | } 218 | } 219 | } 220 | 221 | if ((strcmp(key, "mapname") == 0) && !server->map_name) { 222 | server->map_name = strdup(value); 223 | } else if ((strcmp(key, "hostname") == 0) && !server->server_name) { 224 | server->server_name = strdup(value); 225 | } else if (strcmp(key, "hostport") == 0) { 226 | change_server_port(server, atoi(value), 0); 227 | } else if (strcmp(key, "maxplayers") == 0) { 228 | server->max_players = atoi(value); 229 | } else if (strcmp(key, "numplayers") == 0) { 230 | server->num_players = atoi(value); 231 | } else if ((strcmp(key, server->type->game_rule) == 0) && !server->game) { 232 | server->game = strdup(value); 233 | add_rule(server, key, value, NO_FLAGS); 234 | } else if (strcmp(key, "final") == 0) { 235 | final = 1; 236 | if (id_minor > server->saved_data.pkt_max) { 237 | server->saved_data.pkt_max = id_minor; 238 | } 239 | continue; 240 | } else if ((strncmp(key, "player_", 7) == 0) || (strncmp(key, "playername_", 11) == 0)) { 241 | int no; 242 | if (strncmp(key, "player_", 7) == 0) { 243 | no = atoi(key + 7); 244 | } else { 245 | no = atoi(key + 11); 246 | } 247 | 248 | if (player && (player->number == no)) { 249 | player->name = strdup(value); 250 | player = NULL; 251 | } else if (NULL != (player = get_player_by_number(server, no))) { 252 | player->name = strdup(value); 253 | player = NULL; 254 | } else if (gps_max_players(server)) { 255 | // gps_max_players( server ) due to bf1942 issue 256 | // where the actual no players is correct but more player 257 | // details are returned 258 | player = add_player(server, no); 259 | if (player) { 260 | player->name = strdup(value); 261 | // init to -1 so we can tell if 262 | // we have team info 263 | player->team = -1; 264 | player->deaths = -999; 265 | } 266 | } 267 | } else if (strncmp(key, "teamname_", 9) == 0) { 268 | // Yes plus 1 BF1942 is a silly 269 | players_set_teamname(server, atoi(key + 9) + 1, value); 270 | } else if (strncmp(key, "team_t", 6) == 0) { 271 | players_set_teamname(server, atoi(key + 6), value); 272 | } else if (strncmp(key, "frags_", 6) == 0) { 273 | player = get_player_by_number(server, atoi(key + 6)); 274 | if (NULL != player) { 275 | player->frags = atoi(value); 276 | } 277 | } else if (strncmp(key, "kills_", 6) == 0) { 278 | player = get_player_by_number(server, atoi(key + 6)); 279 | if (NULL != player) { 280 | player->frags = atoi(value); 281 | } 282 | } else if (strncmp(key, "team_", 5) == 0) { 283 | player = get_player_by_number(server, atoi(key + 5)); 284 | if (NULL != player) { 285 | if (!isdigit((unsigned char)*value)) { 286 | player->team_name = strdup(value); 287 | } else { 288 | player->team = atoi(value); 289 | } 290 | server->flags |= FLAG_PLAYER_TEAMS; 291 | } 292 | } else if (strncmp(key, "skin_", 5) == 0) { 293 | player = get_player_by_number(server, atoi(key + 5)); 294 | if (NULL != player) { 295 | player->skin = strdup(value); 296 | } 297 | } else if (strncmp(key, "mesh_", 5) == 0) { 298 | player = get_player_by_number(server, atoi(key + 5)); 299 | if (NULL != player) { 300 | player->mesh = strdup(value); 301 | } 302 | } else if (strncmp(key, "ping_", 5) == 0) { 303 | player = get_player_by_number(server, atoi(key + 5)); 304 | if (NULL != player) { 305 | player->ping = atoi(value); 306 | } 307 | } else if (strncmp(key, "face_", 5) == 0) { 308 | player = get_player_by_number(server, atoi(key + 5)); 309 | if (NULL != player) { 310 | player->face = strdup(value); 311 | } 312 | } else if (strncmp(key, "deaths_", 7) == 0) { 313 | player = get_player_by_number(server, atoi(key + 7)); 314 | if (NULL != player) { 315 | player->deaths = atoi(value); 316 | } 317 | } 318 | // isnum( key[6] ) as halo uses score_tX for team scores 319 | else if ((strncmp(key, "score_", 6) == 0) && isdigit((unsigned char)key[6])) { 320 | player = get_player_by_number(server, atoi(key + 6)); 321 | if (NULL != player) { 322 | player->score = atoi(value); 323 | } 324 | } else if (player && (strncmp(key, "playertype", 10) == 0)) { 325 | player->team_name = strdup(value); 326 | } else if (player && (strncmp(key, "charactername", 13) == 0)) { 327 | player->face = strdup(value); 328 | } else if (player && (strncmp(key, "characterlevel", 14) == 0)) { 329 | player->ship = atoi(value); 330 | } else if (strncmp(key, "keyhash_", 8) == 0) { 331 | // Ensure these dont make it into the rules 332 | } else if (2 == sscanf(key, "%255[^_]_%d", tmp, &player_num)) { 333 | // arbitary player info 334 | player = get_player_by_number(server, player_num); 335 | if (NULL != player) { 336 | player_add_info(player, tmp, value, NO_FLAGS); 337 | } else if (gps_max_players(server)) { 338 | // gps_max_players( server ) due to bf1942 issue 339 | // where the actual no players is correct but more player 340 | // details are returned 341 | player = add_player(server, player_num); 342 | 343 | if (player) { 344 | player->name = NULL; 345 | // init to -1 so we can tell if 346 | // we have team info 347 | player->team = -1; 348 | player->deaths = -999; 349 | } 350 | player_add_info(player, tmp, value, NO_FLAGS); 351 | } 352 | } else { 353 | player = NULL; 354 | add_rule(server, key, value, NO_FLAGS); 355 | } 356 | } 357 | 358 | debug(2, "final %d\n", final); 359 | debug(2, "pkt_id %d\n", server->saved_data.pkt_id); 360 | debug(2, "pkt_max %d\n", server->saved_data.pkt_max); 361 | debug(2, "pkt_index %x\n", server->saved_data.pkt_index); 362 | 363 | if ( 364 | (final && (server->saved_data.pkt_id == 0)) || 365 | (server->saved_data.pkt_max && (server->saved_data.pkt_index >= ((1 << (server->saved_data.pkt_max)) - 1))) || 366 | ((server->num_players < 0) && (id_minor >= 3)) 367 | ) { 368 | return (DONE_FORCE); 369 | } 370 | 371 | return (INPROGRESS); 372 | } 373 | --------------------------------------------------------------------------------