using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
namespace QueryMaster
{
///
/// Represents the connected server.Provides methods to query,listen to server logs and control the server
///
public abstract class Server : IDisposable
{
private bool IsDisposed = false;
private EngineType Type;
private long Latency;
private Logs logs;
private IPEndPoint ServerEndPoint;
internal UdpQuery socket;
internal Rcon RConObj;
///
/// Returns true if server replies only to half life protocol messages.
///
public bool IsObsolete { get; private set; }
private byte[] PlayerChallengeId = null;
private byte[] RuleChallengeId = null;
private bool IsPlayerChallengeId;
private bool IsRuleChallengeId;
internal Server(IPEndPoint address, EngineType type, bool? isObsolete, int sendTimeOut, int receiveTimeOut)
{
ServerEndPoint = address;
socket = new UdpQuery(address, sendTimeOut, receiveTimeOut);
IsDisposed = false;
Type = type;
if (isObsolete == null)
{
try
{
if (socket.GetResponse(QueryMsg.ObsoleteInfoQuery, Type)[0] == 0x6D)
IsObsolete = true;
}
catch (SocketException e)
{
if (e.ErrorCode == 10060)
IsObsolete = false;
return;
}
}
else
IsObsolete = isObsolete == true ? true : false;
}
///
/// Retrieves information about the server
///
/// Instance of ServerInfo class
public virtual ServerInfo GetInfo(byte[] data = null)
{
// If we already have pre-formed request in data parameter, we will populate Query with it.
byte[] query;
if (data != null && data.Length > 0)
{
query = data;
}
else
{
if (IsObsolete)
query = QueryMsg.ObsoleteInfoQuery;
else
query = QueryMsg.InfoQuery;
}
var sw = Stopwatch.StartNew();
var recvData = socket.GetResponse(query, Type);
sw.Stop();
Latency = sw.ElapsedMilliseconds;
try
{
// first, let's check if we have a challenge received instead of S2A_INFO_SRC
// according to https://steamcommunity.com/discussions/forum/14/2974028351344359625/
// server can reply with S2C_CHALLENGE in form of 0x41(4 byte) if it suspects client
// in IP spoofing
if (recvData[0] == 0x41)
{
// we will get current Query and append recvData to its end. Skipping 0x41 and
// honoring the trailing 0x00 at the Query end
var signedQuery = new byte[query.Length + (recvData.Length - 1)];
query.CopyTo(signedQuery, 0);
for (int bId = (recvData.Length - 1); bId > 0; bId--)
{
signedQuery[signedQuery.Length - bId] = recvData[recvData.Length - bId];
}
// finally, we are calling GetInfo() recursively with our new signed query
// to get pass the S2C_CHALLENGE
return GetInfo(signedQuery);
}
switch (recvData[0])
{
case 0x49:
return Current(recvData);
case 0x6D:
return Obsolete(recvData);
default:
throw new InvalidHeaderException("packet header is not valid");
}
}
catch (Exception e)
{
e.Data.Add("ReceivedData", recvData);
throw;
}
}
private ServerInfo Current(byte[] data)
{
Parser parser = new Parser(data);
if (parser.ReadByte() != (byte)ResponseMsgHeader.A2S_INFO)
throw new InvalidHeaderException("A2S_INFO message header is not valid");
ServerInfo server = new ServerInfo();
server.IsObsolete = false;
server.Protocol = parser.ReadByte();
server.Name = parser.ReadString();
server.Map = parser.ReadString();
server.Directory = parser.ReadString();
server.Description = parser.ReadString();
server.Id = parser.ReadShort();
server.Players = parser.ReadByte();
server.MaxPlayers = parser.ReadByte();
server.Bots = parser.ReadByte();
server.ServerType = (new Func(() => { switch ((char)parser.ReadByte()) { case 'l':return "Listen"; case 'd':return "Dedicated"; case 'p':return "SourceTV"; } return ""; }))();
server.Environment = (new Func(() => { switch ((char)parser.ReadByte()) { case 'l':return "Linux"; case 'w':return "Windows"; case 'm':return "Mac"; } return ""; }))();
server.IsPrivate = Convert.ToBoolean(parser.ReadByte());
server.IsSecure = Convert.ToBoolean(parser.ReadByte());
if (server.Id >= 2400 && server.Id <= 2412)
{
TheShip ship = new TheShip();
switch (parser.ReadByte())
{
case 0: ship.Mode = "Hunt"; break;
case 1: ship.Mode = "Elimination"; break;
case 2: ship.Mode = "Duel"; break;
case 3: ship.Mode = "Deathmatch"; break;
case 4: ship.Mode = "VIP Team"; break;
case 5: ship.Mode = "Team Elimination"; break;
default: ship.Mode = ""; break;
}
ship.Witnesses = parser.ReadByte();
ship.Duration = parser.ReadByte();
server.ShipInfo = ship;
}
server.GameVersion = parser.ReadString();
if (parser.HasUnParsedBytes)
{
byte edf = parser.ReadByte();
ExtraInfo info = new ExtraInfo();
info.Port = (edf & 0x80) > 0 ? parser.ReadShort() : (short)0;
info.SteamID = (edf & 0x10) > 0 ? parser.ReadInt() : 0;
if ((edf & 0x40) > 0)
info.SpecInfo = new SourceTVInfo() { Port = parser.ReadShort(), Name = parser.ReadString() };
info.Keywords = (edf & 0x20) > 0 ? parser.ReadString() : string.Empty;
info.GameId = (edf & 0x10) > 0 ? parser.ReadInt() : 0;
server.Extra = info;
}
server.Address = socket.Address.Address + ":" + socket.Address.Port;
server.Ping = Latency;
return server;
}
private ServerInfo Obsolete(byte[] data)
{
Parser parser = new Parser(data);
if (parser.ReadByte() != (byte)ResponseMsgHeader.A2S_INFO_Obsolete)
throw new InvalidHeaderException("A2S_INFO(obsolete) message header is not valid");
ServerInfo server = new ServerInfo();
server.IsObsolete = true;
server.Address = parser.ReadString();
server.Name = parser.ReadString();
server.Map = parser.ReadString();
server.Directory = parser.ReadString();
server.Id = Util.GetGameId(parser.ReadString());
server.Players = parser.ReadByte();
server.MaxPlayers = parser.ReadByte();
server.Protocol = parser.ReadByte();
server.ServerType = (new Func(() => { switch ((char)parser.ReadByte()) { case 'L':return "non-dedicated server"; case 'D':return "dedicated"; case 'P':return "HLTV server"; } return ""; }))();
server.Environment = (new Func(() => { switch ((char)parser.ReadByte()) { case 'L':return "Linux"; case 'W':return "Windows"; } return ""; }))();
server.IsPrivate = Convert.ToBoolean(parser.ReadByte());
byte mod = parser.ReadByte();
server.IsModded = mod > 0 ? true : false;
if (server.IsModded)
{
Mod modinfo = new Mod();
modinfo.Link = parser.ReadString();
modinfo.DownloadLink = parser.ReadString();
parser.ReadByte();//0x00
modinfo.Version = parser.ReadInt();
modinfo.Size = parser.ReadInt();
modinfo.IsOnlyMultiPlayer = parser.ReadByte() > 0 ? true : false;
modinfo.IsHalfLifeDll = parser.ReadByte() < 0 ? true : false;
server.ModInfo = modinfo;
}
server.IsSecure = Convert.ToBoolean(parser.ReadByte());
server.Bots = parser.ReadByte();
server.GameVersion = "server is obsolete,does not provide this information";
server.Ping = Latency;
return server;
}
///
/// Retrieves information about the players currently on the server
///
/// ReadOnlyCollection of Player instances
public virtual ReadOnlyCollection GetPlayers()
{
byte[] recvData = null;
if (IsObsolete)
{
recvData = socket.GetResponse(QueryMsg.ObsoletePlayerQuery, Type);
}
else
{
if (PlayerChallengeId == null)
{
recvData = GetPlayerChallengeId();
if (IsPlayerChallengeId)
PlayerChallengeId = recvData;
}
if (IsPlayerChallengeId)
recvData = socket.GetResponse(Util.MergeByteArrays(QueryMsg.PlayerQuery, PlayerChallengeId), Type);
}
try
{
Parser parser = new Parser(recvData);
if (parser.ReadByte() != (byte)ResponseMsgHeader.A2S_PLAYER)
throw new InvalidHeaderException("A2S_PLAYER message header is not valid");
int playerCount = parser.ReadByte();
List players = new List(playerCount);
for (int i = 0; i < playerCount; i++)
{
parser.ReadByte();//index,always equal to 0
players.Add(new Player()
{
Name = parser.ReadString(),
Score = parser.ReadInt(),
Time = TimeSpan.FromSeconds(parser.ReadFloat())
});
}
if (playerCount == 1 && players[0].Name == "Max Players")
return null;
return players.AsReadOnly();
}
catch (Exception e)
{
e.Data.Add("ReceivedData", recvData);
throw;
}
}
///
/// Retrieves server rules
///
/// ReadOnlyCollection of Rule instances
public ReadOnlyCollection GetRules()
{
byte[] recvData = null;
if (IsObsolete)
{
recvData = socket.GetResponse(QueryMsg.ObsoleteRuleQuery, Type);
}
else
{
if (RuleChallengeId == null)
{
recvData = GetRuleChallengeId();
if (IsRuleChallengeId)
RuleChallengeId = recvData;
}
if (IsRuleChallengeId)
recvData = socket.GetResponse(Util.MergeByteArrays(QueryMsg.RuleQuery, RuleChallengeId), Type);
}
try
{
Parser parser = new Parser(recvData);
if (parser.ReadByte() != (byte)ResponseMsgHeader.A2S_RULES)
throw new InvalidHeaderException("A2S_RULES message header is not valid");
int count = parser.ReadShort();//number of rules
List rules = new List(count);
for (int i = 0; i < count; i++)
{
rules.Add(new Rule() { Name = parser.ReadString(), Value = parser.ReadString() });
}
return rules.AsReadOnly();
}
catch (Exception e)
{
e.Data.Add("ReceivedData", recvData);
throw;
}
}
private byte[] GetPlayerChallengeId()
{
byte[] recvBytes = null;
byte header = 0;
Parser parser = null;
recvBytes = socket.GetResponse(QueryMsg.PlayerChallengeQuery, Type);
try
{
parser = new Parser(recvBytes);
header = parser.ReadByte();
switch (header)
{
case (byte)ResponseMsgHeader.A2S_SERVERQUERY_GETCHALLENGE: IsPlayerChallengeId = true; return parser.GetUnParsedData();
case (byte)ResponseMsgHeader.A2S_PLAYER: IsPlayerChallengeId = false; return recvBytes;
default: throw new InvalidHeaderException("A2S_SERVERQUERY_GETCHALLENGE message header is not valid");
}
}
catch (Exception e)
{
e.Data.Add("ReceivedData", recvBytes);
throw;
}
}
private byte[] GetRuleChallengeId()
{
byte[] recvBytes = null;
byte header = 0;
Parser parser = null;
recvBytes = socket.GetResponse(QueryMsg.RuleChallengeQuery, Type);
try
{
parser = new Parser(recvBytes);
header = parser.ReadByte();
switch (header)
{
case (byte)ResponseMsgHeader.A2S_SERVERQUERY_GETCHALLENGE: IsRuleChallengeId = true; return BitConverter.GetBytes(parser.ReadInt());
case (byte)ResponseMsgHeader.A2S_RULES: IsRuleChallengeId = false; return recvBytes;
default: throw new InvalidHeaderException("A2S_SERVERQUERY_GETCHALLENGE message header is not valid");
}
}
catch (Exception e)
{
e.Data.Add("ReceivedData", recvBytes);
throw;
}
}
///
/// Listen to server logs.
///
/// Local port
/// Instance of Log class
/// Receiver's socket address must be added to server's logaddress list before listening
public Logs GetLogs(int port)
{
logs = new Logs(Type, port, ServerEndPoint);
return logs;
}
///
/// Gets valid rcon object that can be used to send rcon commands to server
///
/// Rcon password of server
/// Instance of Rcon class
public abstract Rcon GetControl(string pass);
///
/// Gets Round-trip delay time
///
/// Elapsed milliseconds
///
public long Ping()
{
Stopwatch sw = null;
if (IsObsolete)
{
sw = Stopwatch.StartNew();
byte[] msg = socket.GetResponse(QueryMsg.ObsoletePingQuery, Type);
sw.Stop();
}
else
{
sw = Stopwatch.StartNew();
byte[] msg = socket.GetResponse(QueryMsg.InfoQuery, Type);
sw.Stop();
}
return sw.ElapsedMilliseconds;
}
///
/// Disposes all the resources used by the server object
///
public void Dispose()
{
if (IsDisposed)
return;
if (socket != null)
socket.Dispose();
if (RConObj != null)
RConObj.Dispose();
if (logs != null)
logs.Dispose();
IsDisposed = true;
}
}
}