Using nodejs to read counter strike server info

NodeJs is a very powerfull tool for creating server side applications. Nodejs is a event-driven I/O JavaScript environment based on V8, wich is the engine for the chrome browser and is very fast, . This article shows how to read counter strike server info trought udp protocol wich the server runs. On default the server runs on the port 27015, and to read the info you need to send some pakets for the what you need: the server details, the players list or the server rules.

var Packet = {
	ping : [0xFF,0xFF,0xFF,0xFF,0x69],
	challenge : [0xFF,0xFF,0xFF,0xFF,0x56,0x00,0x00,0x00,0x00],
	details : [0xFF,0xFF,0xFF,0xFF, 0x54, 0x53, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x45, 0x6E, 0x67, 0x69, 0x6E, 0x65, 0x20, 0x51, 0x75, 0x65, 0x72, 0x79, 0x00],
	players : [0xFF,0xFF,0xFF,0xFF,0x55],
	rules : [0xFF,0xFF,0xFF,0xFF,0x56]
};

For every information wich server can serve you need to send a packet from above and you will receive the binary data. To read more about the counter strike server queries click here.

Here is the code to take a look and next i will explain much more about it.


var util = require('util');
var DGram = require('dgram');
var EventEmitter = require('events').EventEmitter;

var Packet = {
	ping : [0xFF,0xFF,0xFF,0xFF,0x69],
	challenge : [0xFF,0xFF,0xFF,0xFF,0x56,0x00,0x00,0x00,0x00],
	details : [0xFF,0xFF,0xFF,0xFF, 0x54, 0x53, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x45, 0x6E, 0x67, 0x69, 0x6E, 0x65, 0x20, 0x51, 0x75, 0x65, 0x72, 0x79, 0x00],
	players : [0xFF,0xFF,0xFF,0xFF,0x55],
	rules : [0xFF,0xFF,0xFF,0xFF,0x56]
};

var XBuffer = function(data)
{
	this.data = data;
	this.position = 0;
}

XBuffer.prototype.readString = function()
{
	var string = '';

	while(this.data[this.position] != 0x00)
	{
		string += String.fromCharCode(this.data[this.position]);
		this.position++;

		if(this.position > this.data.length)
			break;
	}

	//skip  0x00
	this.position++;

	return string;
}
XBuffer.prototype.skip = function(n)
{
	n = n || 1;
	this.position += n;
}
XBuffer.prototype.available = function()
{
	return Math.max(this.data.length - this.position, 0);
}
XBuffer.prototype.read = function(n)
{
	var b = new Buffer(n);
	n = n || 1;
	for(var i=0; i < n; i++)
	{
		b[i] = this.data[this.position+n];
		this.position++;
	}

	return new XBuffer(b);
}
XBuffer.prototype.readByte = function()
{
	var value = this.data[this.position];
	this.position++;
	return String.fromCharCode(value);
}
XBuffer.prototype.lookAhead = function(n)
{
	var tempPos = this.position;
	var buff = this.read(n);
	this.position=tempPos;
	return buff;
}

XBuffer.prototype.readUInt8 = function()
{
	var value = this.data.readUInt8(this.position);
	this.position++;
	return value;
}
XBuffer.prototype.readInt8 = function()
{
	var value = this.data.readInt8(this.position);
	this.position++;
	return value;
}
XBuffer.prototype.readInt16LE = function()
{
	var value = this.data.readInt16LE(this.position);
	this.position+=2;
	return value;
}
XBuffer.prototype.readInt16BE = function()
{
	var value = this.data.readInt16BE(this.position);
	this.position+=2;
	return value;
}
XBuffer.prototype.readInt32LE = function()
{
	var value = this.data.readInt32LE(this.position);
	this.position+=4;
	return value;
}
XBuffer.prototype.readFloatLE = function()
{
	var value = this.data.readFloatLE(this.position);
	this.position+=4;
	return value;
}
XBuffer.prototype.toString = function()
{
	return "[XBuffer " + this.data + "]";
}

function parseRules( data )
{
	var d = {};
	var b = new XBuffer(data);

	b.skip(5);

	var count = b.readInt16LE();

	if (count == 65535)
	{
		b.skip();
		count = b.readInt16LE();
	}

	d.num_rules = count;

	while(b.available())
	{
		d[b.readString()]=b.readString();
	}

	return d;
}

function parseDetails( data )
{
	b = new XBuffer(data);
	var d = {};

	b.skip(4);

	d.type =  b.readInt8();

	if (d.type == 0x6D)
		d.address =  b.readString();
	else
		d.protocol= b.readInt8();

	d.hostname =  b.readString();
	d.map =  b.readString();
	d.game_dir =  b.readString();
	d.game_desc =  b.readString();

	if (d.type != 0x6D) d.steamappid = b.readInt16LE();

	d.num_players= b.readInt8();
	d.max_players= b.readInt8();

	if (d.type == 0x6D) d.protocol = b.readInt8();
	else               d.num_bots = b.readInt8();

	d.dedicated = b.readByte();
	d.os		= b.readByte();
	d.password		= b.readInt8();
	d.secure		= b.readInt8();
	d.version		= b.readInt8();

	return d;
}

function parsePlayers( buffer )
{
	var b = new XBuffer( buffer );

	var players=[];

	b.skip(5);

	var playersn = b.readUInt8();

	for(var i=0; i < playersn; i++)
	{
		players.push({
			id : b.readUInt8(),
			name : b.readString(),
			score : b.readInt32LE(),
			time : b.readFloatLE()
		});
	}

	return players;
}

var Protocol = function(ip, port)
{
	this.ip = ip;
	this.port = port;
	this.client = DGram.createSocket("udp4");

	if(false === (this instanceof Protocol)) {
        return new Protocol(ip, port);
    }

	EventEmitter.call(this);
}

util.inherits(Protocol, EventEmitter);

Protocol.prototype.request = function()
{
	var self = this;

	var data = {};

	self.dgramRequest(new Buffer(Packet['ping']), function(challData,rinfo){
		if(challData[4] == 0x6A)
		{
			//pong received

			//request details
			self.dgramRequest(new Buffer(Packet['details']), function( detailsData, rinfo1){

				//parse server details
				try {
					data.details = parseDetails( detailsData );
				}catch(e){
					self.emit("exception", e);
				}

				//request the server details
				self.dgramRequest( new Buffer(Packet['players']), function(playersData, rinfo2){

					//parse players data and ad it to the data object
					try {
						data.players = parsePlayers( playersData );
					}catch(e) {
						self.emit("exception", e);
					}

					self.dgramRequest( new Buffer(Packet['rules']), function(rulesData, rinfo3){

						//parse server rules
						try {
						data.rules = parseRules( rulesData );
						} catch(e) {
							self.emit("exception", e);
						}

						//dispatch event
						self.emit("success", data);
					});

				});
			});

		}
		else
		{
			//no pong received, server is down
			self.emit("offline");
		}
	});

}

function preprocess( data )
{
	//used if you need to filter data from the server
	return data;
}

Protocol.prototype.dgramRequest = function( buffer, callback )
{
	var self = this;

	var offlineTimeout;

	var _internalCallback = function(data, rinfo)
	{
		data = preprocess( data );

		clearTimeout(offlineTimeout);

		if(callback)
		setTimeout(callback,0, data, rinfo);

		self.client.removeListener("message", _internalCallback);
	}

	offlineTimeout = setTimeout(function(){
		self.emit("offline");
	}, 2000);

	this.client.on("message", _internalCallback);

	this.client.send(buffer, 0, buffer.length, parseInt(this.port), this.ip, function(err, bytes) {
	  if(err){	console.log("Fail to broadcase packet to " + self.ip); }
	});

}

exports.Protocol = Protocol;

As you see there is required the dgram package wich is used for udp communications. UDP uses a simple transmission model without implicit handshaking dialogues for providing reliability, ordering, or data integrity. Thus, UDP provides an unreliable service and datagrams may arrive out of order, appear duplicated, or go missing without notice.

In this code are two objects the XBuffer and Protocol, XBuffer simply increase the reading pointer when reading data, and the Protocol send all pakets, filter data and dispatched as event.

Example of requesting info from the server

self.dgramRequest( new Buffer(Packet['players']), function(playersData, rinfo2) {
//your code here
} );

The function gramRequest is used to make syncron request to the server, send the packet and wait for response. If the response not apear for 2 senconds will trigger the offline event

The requests are sent one after another, parsed for a human readable format and pushed to the data object, when all complete is dispatched the success event ( self.emit(“success”, data); )

For every kind of received info i used functions to parse the binary data, for example, the player data is parsed with parsePlayers function. See the code comments for see how its work.

function parsePlayers( buffer )
{
        //create the xbuffer object with received buffer
	var b = new XBuffer( buffer );

	var players=[];
        //skip 5 unused bytes
	b.skip(5);
        //read a 2 bytes int wich is the number of the players
	var playersn = b.readUInt8();
        //make a loop to players length
	for(var i=0; i < playersn; i++)
	{
		players.push({
			id : b.readUInt8(), // read 2 bytes of int with the id of the player
			name : b.readString(), //read n bytes ending with 0x00 wich is the player name
			score : b.readInt32LE(), //read 4 bytes int with user score
			time : b.readFloatLE() //read 4 bytes float with user playing time
		});
	}

	return players;
}

Mini example to use this code

var Protocol = require("./communication.js").Protocol;

var protocol = new Protocol(server.ip, server.port);

protocol.on("success", function(data){
 //use data
});
protocol.request();

I hope this article helps you, and if you have some questions leave me a message.

5 Comments

  1. yesterday i started looking into querying a counter-strike server (hlds) with node.js.

    And today i get to see your article… wow you did a great job. keep it up.

    I will surely use this on my website in the near future.

    going to try it now :)

    Reply
  2. The code below does not return anything :(
    You can give me a working example please with the ip below.
    thanks

    var Protocol = require(“./query_hlds.js”).Protocol;

    //var protocol = new Protocol(server.ip, server.port);
    var protocol = new Protocol(‘77.220.180.177’,27015);

    console.log(‘Start’);

    protocol.on(“success”, function(data){
    console.log(‘here’);
    console.log(data.toString());
    });

    Reply
  3. Thanks for the code! nice work, unfortunately i’m having a problem with the players info, i got the following exception when the parsePlayers function is called

    {
    name: ‘AssertionError’,
    message: ‘Trying to read beyond buffer length’,
    actual: false,
    expected: true,
    operator: ‘==’
    }

    Any idea to solve this?

    Thanks in advanced!

    Reply

Leave a Comment.