|  | |  |
06-15-2006, 11:45 PM
|
#1 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| socket based CS server query I've been pulling my hair out trying to figure out how to do info queries on counter-strike servers.
I ended up having to use Ethereal to catch UDP packets and find out exactly what I send to the counter-strike server when I do a refresh. The initial query is always the same. It's a padded string which basically says "Source Engine Query.": Quote: |
\377\377\377\377\124\123\157\165\162\143\145\40\10 5\156\147\151\156\145\40\121\165\145\162\171\0
| After sending that message, I get back some basic server info. The address, name, current/max players, etc. Here's a sample response converted to ascii: Quote:
0. ÿÿÿÿm69.90.60.125:27015
1. [euphNET] Pub #1
2. de_nuke
3. cstrike
4. Counter-Strike
5. /dl
6. www.counter-strike.net
7.
| Now in order to get the list of players and scores, I have to send a response to this. It's different for each server, but every time it's a 9 byte string consisting of a static 5 byte header. Only the following 4 bytes are different per server. Here is the key for this particular server:
"\377\377\377\377\125\247\304\302\72"
So far, this 4 byte key hasn't changed the the server I've been testing on over the last few days.
The key is not calculated on the server name, map name, or number of players as far as I can tell, otherwise the key I've been using for the test server wouldn't be valid anymore. Maybe it's based on the server address? If it is, I haven't been able to figure out how it's derived.
I spent most of the day today coming up with a regular expression to extract player id's, names and scores from this mess of octal values. As soon as I figure out how to generate "callback" keys, I'll start turning this into a class, and work on rcon. This is just a project to help me learn socket programming. I'd never touched them before; there was a bit of a stigma around the concept for me. |
| |
06-16-2006, 10:54 AM
|
#2 (permalink)
| | Moderator
Join Date: May 2002 Location: us.ca
Posts: 4,532
| i spent some time a while ago on this. i had a class working for 1.6 (originally coded by madhatter) but then the implementation changed with source. lots of tweaking, trial, and error and i finally got it working. the last few lines instanciates the class and runs the query. enjoy PHP Code: <? function get_float32($fourchars) { $bin=''; for($loop = 0; $loop <= 3; $loop++) { $bin = str_pad(decbin(ord(substr($fourchars, $loop, 1))), 8, '0', STR_PAD_LEFT).$bin; } $exponent = bindec(substr($bin, 1, 8)); $exponent = ($exponent)? $exponent - 127 : $exponent; if($exponent) { $int = bindec('1'.substr($bin, 9, $exponent)); $dec = bindec(substr($bin, 9 + $exponent)); $time = "$int.$dec"; return number_format($time / 60, 2); } else { return 0.0; } }
class sourceQueryCS{
function sourceQueryCS($ip,$port){
$this->ip=$ip; $this->port=$port; $this->address=$ip.":".$port; $this->hostname = ""; $this->map = ""; $this->mod = ""; $this->modname = ""; $this->active = ""; $this->max = ""; $this->cvars = array(); $this->players = array(); $this->excluded_cvars = array(); /* // you can define cvars you wish to exclude // this may be useful if you are looping through // the cvar array instead of just calling individual cvars $this->excluded_cvars = array( "mp_falldamage", "mp_weaponstay", "mp_forcerespawn", "mp_autocrosshair", "decalfrequency", "coop", "mp_teamlist", "mp_allowNPCs", "sv_stopspeed", "sv_noclipaccelerate", "sv_noclipspeed", "sv_specaccelerate", "sv_specspeed", "sv_specnoclip", "sv_maxspeed", "sv_accelerate", "sv_airaccelerate", "sv_wateraccelerate", "sv_waterfriction", "sv_rollspeed", "sv_rollangle", "sv_friction", "sv_bounce", "sv_stepsize", "r_VehicleViewDampen", "r_JeepViewDampenFreq", "r_JeepViewDampenDamp", "r_JeepViewZHeight", "r_AirboatViewDampenFreq", "r_AirboatViewDampenDamp", "r_AirboatViewZHeight", "sv_pausable" ); */ $this->_sock = fsockopen("udp://".$this->ip,$this->port, $errno, $errstr, 3);
if (!$this->_sock) { echo "unable to connect to ".$this->ip.":".$this->port; exit; } $this->getInfo(); $this->getRules(); $this->getPlayers();
fclose($this->_sock); } function getInfo(){ $array = array(); $query=chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0x54); fwrite($this->_sock, $query); socket_set_timeout($this->_sock, 2,0); $buffer=fread($this->_sock,1); $stat=socket_get_status($this->_sock); $buffer.=fread($this->_sock, $stat["unread_bytes"]); $buffer=substr($buffer,6); $text=""; $count=0; $arr=array(0); do { $tmp=substr($buffer,0,1);$buffer=substr($buffer,1); if (!ord($tmp)) { $array[$count++]=$text; $text=""; } else { $text.=$tmp; } } while ($count<5); for($i=0;$i<=6;$i++, $count++) { $tmp=substr($buffer,0,1);$buffer=substr($buffer,1); if($count==8 || $count==9) $array[$count]=$tmp; else $array[$count]=ord($tmp); } //count = 12 if($array[$count-1]) { //if ismod do { $tmp=substr($buffer,0,1);$buffer=substr($buffer,1); if (ord($tmp)!=0) $array[$count].=$tmp; // mod website [12] } while(ord($tmp)!=0); $count++; do { $tmp=substr($buffer,0,1);$buffer=substr($buffer,1); if (ord($tmp)!=0) $array[$count].=$tmp; // mod FTP [13] } while(ord($tmp)!=0); $count++; $array[$count++]=ord(substr($buffer,0,1)); $buffer=substr($buffer,1); //Dummy bit? [14] o_0 -- SHOULD be server-only bit... ^_^ $tmp=substr($buffer,0,4);$buffer=substr($buffer,4); for($j=0;$j<4;$j++) { $array[$count]+=(pow(256,$j) * ord(substr($tmp,$j,1))); //Ver [15] } $count++; $tmp=substr($buffer,0,4);$buffer=substr($buffer,4); for($j=0;$j<4;$j++) { $array[$count]+=(pow(256,$j) * ord(substr($tmp,$j,1))); //Size [16] } $count++; $array[$count++]=ord(substr($buffer,0,1));$buffer=substr($buffer,1); //server-only [17] $array[$count++]=ord(substr($buffer,0,1));$buffer=substr($buffer,1); //custom client.dll [18] $array[$count++]=ord(substr($buffer,0,1));$buffer=substr($buffer,1); //Secure! [19] } else { for($i=0;$i<8;$i++) $array[$count++]='\0'; } $this->hostname = $array[0]; $this->map = $array[1]; $this->mod = $array[2]; $this->modname = $array[3]; $this->active = $array[6]; $this->max = $array[7]; } function getplayers(){ $query=chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0x55); fwrite($this->_sock, $query); socket_set_timeout($this->_sock, 2,0); $buffer=fread($this->_sock,1); $stat=socket_get_status($this->_sock); $buffer.=fread($this->_sock, $stat["unread_bytes"]); $buffer=substr($buffer,5); $count=ord(substr($buffer,0,1)); //Num active players $buffer=substr($buffer,1); $tfrags=""; $ttime=0; $array=array(0); for($i=0;$i<$count;$i++){ $rfrags=0.0; $rtime=0; $stime=0; $tind=ord(substr($buffer,0,1)); $buffer=substr($buffer,1); $tname=""; do { $tmp=substr($buffer,0,1); $buffer=substr($buffer,1); $tname.=$tmp; }while(ord($tmp)!=0); $tfrags=substr($buffer,0,4); $buffer=substr($buffer,4); for($j=0;$j<4;$j++) { $rfrags+=(pow(256,$j) * ord(substr($tfrags,$j,1))); } if($rfrags > 2147483648) { $rfrags-=4294967296; } $tmp=substr($buffer,0,4); $buffer=substr($buffer,4); $rtime=get_float32($tmp); $array[$i]=array("index" => $tind,"name" => $tname,"frags" => $rfrags, "time" => $rtime); } $this->players = $array; } function getRules(){ $array = array(); $rules = array(); $query=chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0x56); fwrite($this->_sock, $query); socket_set_timeout($this->_sock, 2,0); $buffer=fread($this->_sock,1); $stat=socket_get_status($this->_sock); $buffer.=fread($this->_sock, $stat["unread_bytes"]);
$array = explode(chr(0),$buffer); $count = (count($array)-1); for($i=1;$i<$count;$i++){ if(in_array($array[$i],$this->excluded_cvars)){ $i++; continue; } $rules[$array[$i]]=$array[++$i]; } $this->cvars = $rules; } }
header("content-type: text/plain"); $sq = new sourceQueryCS("192.18.1.250",27016); print_r($sq); ?>
__________________ Mike |
| |
06-16-2006, 12:05 PM
|
#3 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| Awesome, there're definitely a few things in there I could use. Specifically, the function you're using to turn those arbitrary looking numbers at the end of each player info row into a time. Unfortunately I think steam hacked up the socket commands and responses.
Short queries like the getInfo() query Quote: |
chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0x54)
| don't work anymore. The server won't send back a response. The longer info query I'm sending has the same 5th character though. Same with the player list query.
I think steam made some changes to make it harder for third-parties to communicate with their servers. I'm still stumped about how this "key" is being generated |
| |
06-16-2006, 04:11 PM
|
#4 (permalink)
| | Moderator
Join Date: May 2002 Location: us.ca
Posts: 4,532
| ah that sucks that they changed it. =/
__________________ Mike |
| |
06-16-2006, 04:59 PM
|
#5 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| I know =/ Any idea how to figure out what string to send with the player list command? It has the same 5th character as the one you used in your class, but now it's followed by 4 practically arbitrary chars. *arg* |
| |
06-16-2006, 06:38 PM
|
#6 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| Okay, I found the answer. In order to get the "key" to retrieve the player list, and server settings, you need to issue a getchallenge request to the server. It then replies with "FF FF FF FF 41" followed by the four character challenge key.
This was found here, on the wiki valve developer site: http://developer.valvesoftware.com/w...Server_Queries |
| |
06-17-2006, 04:57 AM
|
#7 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| Okay, I have a working class now. It's modeled after yours Mike. I hate modeling my code after anyone elses work; I feel like I'm cheating myself. But I liked the overall layout of your class. I certainly didn't copy it though. I more or less just took the framework. Except for the get_float32 function. I stole that. I barely understand what it's doing, even after writing my own function to build the 32bit score. I didn't realize the score was 32 bit until I found my class didn't work on servers where player scores were more than 255. Even then, it wasn't until I found that the score was considered as "long" instead of "byte", in the valve development wiki site.
Okay. 5am. Time for bed. |
| |
06-17-2006, 07:26 AM
|
#8 (permalink)
| | Newbie
Join Date: Jun 2002 Location: Denmark
Posts: 1,726
| Quote: |
I hate modeling my code after anyone elses work; I feel like I'm cheating myself.
| Half the work as a programmer, is borrowing code from others, why invent the wheel twice for something. |
| |
06-17-2006, 12:34 PM
|
#9 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| Oh, believe me, I know. The difference is I'm still learning quite a bit. It's generally better for me to do things on my own the first few times to get the swing of it |
| |
06-18-2006, 01:18 AM
|
#10 (permalink)
| | PHP Student
Join Date: Oct 2004 Location: Forest Grove, OR
Posts: 151
| This is still a little over my head Hey, mike, or anyone really, would you be able to change this function so that it will work with negative numbers? I'm still not sure how to convert 32 bits into negative values. It works fine for converting positive frags, but if someone has a negative score, it spits out insanely large numbers. PHP Code: function get_long32($num) {
$num = explode(',', $num);
$binary_num = "";
for ($i = 3; $i >= 0; $i--) {
$binary_num .= str_pad(decbin($num[$i]), 8, '0', STR_PAD_LEFT);
}
return bindec($binary_num);
}
Right now it takes four comma delimited dec values, explodes it, reassembles the string in reverse order with each individual number converted to binary format, then returns that compiled number in dec form.
If you have the time to rebuild the function that would be fantastic. If you have the time to explain how the proper conversion works, that'd be doubly fantastic, 'cause I realize I took a shortcut on this one. |
| | | | |