public/SourceQuery.ps1
############### # Libraries # ############### # Source Query documnetation from: https://developer.valvesoftware.com/wiki/Server_Queries#Multi-packet_Response_Format class SourceQueryBuffer { SourceQueryBuffer([byte[]]$buffer) { $this.buffer = $buffer $bufferTmp = $this.buffer.Clone() [array]::Reverse($bufferTmp) $this.lastNullCharacterPosition = $this.buffer.length - 1 - $bufferTmp.IndexOf( [byte]0 ) } [byte[]]$buffer [int] hidden $position [int] hidden $lastNullCharacterPosition [int]PeekByte() { $data = $this.buffer[ $this.position ] return $data } [int]GetByte() { $this.position++ $data = $this.buffer[ $this.position - 1 ] return $data } [int]GetShort() { $this.position += 2 $data = [BitConverter]::ToInt16($this.buffer, $this.position - 2) # return $data } [int]GetLong() { $this.position += 4 $data = [BitConverter]::ToInt32($this.buffer, $this.position - 4) return $data } [int]GetLongLong() { $this.position += 8 $data = [BitConverter]::ToInt64($this.buffer, $this.position - 8) return $data } [float]GetFloat() { #$bytes = $this.buffer[ ($this.position) .. ($this.position + 3) ] #[float[]]$floatArr = [float]($bytes.length / 4) # for ($i = 0; $i -lt $floatArr.Length; $i++) { # if ([BitConverter]::IsLittleEndian) { # [Array]::Reverse($bytes, $i * 4, 4) # } # $floatArr[$i] = [BitConverter]::ToSingle($bytes, $i * 4) # } #return $floatArr[0] $bytes = $this.buffer[ ($this.position) .. ($this.position + 3) ] if ([BitConverter]::IsLittleEndian) { #[Array]::Reverse($bytes) } $float = [BitConverter]::ToSingle($bytes, 0) $this.position += 4 return $float } [string]GetString() { $bufferRemaining = $this.buffer[ $($this.position) .. $( $this.buffer.Length - 1 ) ] $nullTerminatorPosition = $bufferRemaining.IndexOf( [byte]0 ) $str = [System.Text.Encoding]::UTF8.GetString($bufferRemaining[ 0 .. $nullTerminatorPosition ]) $this.position += $nullTerminatorPosition + 1 return $str.Trim("`0") } [bool]HasMore() { if ($this.position -lt $this.lastNullCharacterPosition) { return $true } return $false } # Returns all bytes after the last Null Character Position until end of byte array [byte[]]GetRemainingBytes() { if ($this.lastNullCharacterPosition -eq -1) { return $null }else { $bytes = $this.buffer[ $($this.lastNullCharacterPosition + 1) .. $($this.buffer.Length - 1) ] return $bytes } return $null } [string]GetRemainingString() { $bytes = $this.GetRemainingBytes() if ($bytes -ne $null) { return [System.Text.Encoding]::UTF8.GetString( $bytes ) } return '' } } <# .SYNOPSIS Performs a Source Query. .DESCRIPTION Performs a Source Query. .PARAMETER Address DNS or IP address. .PARAMETER Port Port. .PARAMETER Engine Source engine. May be one of the following: 'GoldSource', 'Source'. .PARAMETER Type Query type. Mya be one of the following: 'info', 'players', 'rules', 'ping'. .EXAMPLE # Source Engine # A2S_INFO query. Returns a hashtable of server metadata SourceQuery -Address $address -Port $port -Engine 'Source' -Type 'info' # A2S_PLAYER query. Returns a hashtable of players SourceQuery -Address $address -Port $port -Engine 'Source' -Type 'players' # A2S_RULES query, Returns a hashtable of server cvars SourceQuery -Address $address -Port $port -Engine 'Source' -Type 'rules' # A2A_PING query. Returns a hashtable of whether the ping was successful SourceQuery -Address $address -Port $port -Engine 'Source' -Type 'ping' .EXAMPLE # GoldSource Engine # A2S_INFO query - Returns a hashtable of server metadata SourceQuery -Address $address -Port $port -Engine 'GoldSource' -Type 'info' # A2S_PLAYER query. Returns a hashtable of players SourceQuery -Address $address -Port $port -Engine 'GoldSource' -Type 'players' # A2S_RULES query, Returns a hashtable of server cvars SourceQuery -Address $address -Port $port -Engine 'GoldSource' -Type 'rules' # A2A_PING query. Returns a hashtable of whether the ping was successful SourceQuery -Address $address -Port $port -Engine 'GoldSource' -Type 'ping' .NOTES Queries: https://developer.valvesoftware.com/wiki/Server_queries Goldsource: https://developer.valvesoftware.com/wiki/Goldsource Source: https://developer.valvesoftware.com/wiki/Source A2A_PING is no longer supported on Counter Strike: Source and Team Fortress 2 servers, and is considered a deprecated feature. See: https://developer.valvesoftware.com/wiki/Server_Queries#A2A_PING #> function SourceQuery { [CmdletBinding()] param( [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Address , [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [int]$Port , [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet('Goldsource', 'Source')] [string]$Engine , [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet('info', 'players', 'rules', 'ping')] [string]$Type ) try { # Determine the IP $Address = Resolve-DNS -Address $Address Write-Verbose "Sending SourceQuery to $Address`:$Port" # Constants (Request Body) $A2S_INFO = 0x54 $A2S_PLAYER = 0x55 $A2S_RULES = 0x56 $A2A_PING = 0x69 $A2S_SERVERQUERY_GETCHALLENGE = 0x57 # Deprecated if (!$Address) { throw "Invalid address" } # Set up UDP Socket $remoteEP = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($Address), $Port) $udpClient = New-Object System.Net.Sockets.UdpClient $udpClient.Client.SendTimeout = 500 $udpClient.Client.ReceiveTimeout = 500 $udpClient.Connect($remoteEP) $requestBody = '' if ($Type -match 'info') { $requestBody = $A2S_INFO }elseif ($Type -match 'players') { $requestBody = $A2S_PLAYER }elseif ($Type -match 'rules') { $requestBody = $A2S_RULES }elseif ($Type -match 'ping') { $requestBody = $A2A_PING } function BuildPacket () { $pack = @(255,255,255,255) + $requestBody + [System.Text.Encoding]::UTF8.GetBytes('Source Engine Query') + 0 $pack } function SendPacket ($pack) { Debug-Packet $MyInvocation.MyCommand.Name $pack $udpClient.Send($pack, $pack.Length) > $null } function ReceivePacket { $pack = $udpClient.Receive([ref]$remoteEP) Debug-Packet $MyInvocation.MyCommand.Name $pack $pack } function GetQueryData ([byte[]]$rPack) { if ($requestBody -eq $A2S_INFO) { $pack = BuildPacket SendPacket $pack $rPack = ReceivePacket if (!$rPack.Length) { return } $buffer = [SourceQueryBuffer]::New($rPack) $Junk = $buffer.GetLong() $Header = $buffer.GetByte() if ($Header -eq 0x6C) { # 'l' - Banned by the server. $Info = [ordered]@{ Message = $buffer.GetString() Banned = $true } }else { if ($Header -eq 0x6D) { # 'm' - Obsolute Goldsource $Info = [ordered]@{ Address = $buffer.GetString() Name = $buffer.GetString() Map = $buffer.GetString() # .... } }else { $Info = [ordered]@{ Protocol = $buffer.GetByte() Name = $buffer.GetString() Map = $buffer.GetString() Folder = $buffer.GetString() Game = $buffer.GetString() ID = $buffer.GetShort() Players = $buffer.GetByte() Max_players = $buffer.GetByte() Bots = $buffer.GetByte() Server_type = $buffer.GetByte() Environment = & { switch ( [System.Text.Encoding]::UTF8.GetString($buffer.GetByte()) ) { 'l' { 'linux'; break } 'w' { 'windows'; break } 'm' { 'mac'; break } default: { '' } } } Visibility = if ($buffer.GetByte() -eq 0) { 'public' } else { 'public'} VAC = if ($buffer.GetByte() -eq 0) { 'secured' } else { 'unsecured' } } if ($Info['ID'] -eq 2400) { # AppID 2400 is The Ship $Info['Mode'] = $buffer.GetByte() $Info['Witnesses'] = $buffer.GetByte() $Info['Duration '] = $buffer.GetByte() } $Info['Version'] = $buffer.GetString() $extraDataFlag = $buffer.GetByte() if ($extraDataFlag -band 0x80) { # Server's game port number $Info['Port'] = $buffer.GetShort() }elseif ($extraDataFlag -band 0x80) { # Server's SteamID $Info['SteamID'] = $buffer.GetLongLong() }elseif ($extraDataFlag -band 0x40) { # Source TV port and name $Info['Port'] = $buffer.GetShort() $Info['Name'] = $buffer.GetString() }elseif ($extraDataFlag -band 0x20) { # Tags that describe the game according to the server (for future use.) $Info['Keywords'] = $buffer.GetString() }elseif ($extraDataFlag -band 0x01) { # The server's 64-bit GameID. If this is present, a more accurate AppID is present in the low 24 bits. The earlier AppID could have been truncated as it was forced into 16-bit storage. $Info['GameID'] = $buffer.GetLongLong() } } } return $Info }elseif ($requestBody -eq $A2S_PLAYER) { # Send a challenge request $pack = @(255,255,255,255) + $requestBody + @( 0x00, 0x00, 0x00, 0x00) SendPacket $pack $rpack = ReceivePacket if (!$rPack.Length) { return } # Are we banned? $buffer = [SourceQueryBuffer]::New($rPack) $Header = $buffer.GetByte() if ($Header -eq 0x6C) { # 'l' - Banned by the server. $Players = [ordered]@{ Message = $buffer.GetString() Banned = $true } }else { # A2S_PLAYER request $pack = @(255,255,255,255) + $requestBody + $rpack[5..8] SendPacket $pack $rpack = ReceivePacket if (!$rPack.Length) { return } $buffer = [SourceQueryBuffer]::New($rPack) $Junk = $buffer.GetLong() $Header = $buffer.GetByte() $Players = [ordered]@{ Players_count = $buffer.GetByte() Players = [System.Collections.ArrayList]@() } if ($Players['Players_count'] -gt 0) { 1..$Players['Players_count'] | % { $player = [ordered]@{ Index = $buffer.GetByte() Name = $buffer.GetString() Score = $buffer.GetLong() Duration = $buffer.GetFloat() } $duration = [int]($player['Duration']) $duration = New-Timespan -Seconds $duration $player['Duration_hh_mm_ss'] = if ($duration.Hours -gt 0) { $duration.ToString('hh\:mm\:ss') } else { $duration.ToString('mm\:ss') } $Players['Players'].Add( $player ) > $null } } } return $Players }elseif ($requestBody -eq $A2S_RULES) { # Send a challenge request $pack = @(255,255,255,255) + $requestBody + @( 0x00, 0x00, 0x00, 0x00) SendPacket $pack $rpack = ReceivePacket if (!$rPack.Length) { return } # Are we banned? $buffer = [SourceQueryBuffer]::New($rPack) $Junk = $buffer.GetLong() $Header = $buffer.GetByte() if ($Header -eq 0x6C) { # 'l' - Banned by the server. $Rules = [ordered]@{ Message = $buffer.GetString() Banned = $true } }else { # A2S_RULES request $pack = @(255,255,255,255) + $requestBody + $rpack[5..8] SendPacket $pack try { $rPack = '' $Rules = [ordered]@{ Rules_count = 0 Rules = [System.Collections.ArrayList]@() } $cnt = 0 while ($rPack = ReceivePacket) { $buffer = [SourceQueryBuffer]::New($rPack) # Packet Header $packetHeader = $buffer.GetLong() # 4 if ($packetHeader -eq -2) { # PacketID $packetIDTmp = $buffer.GetLong() # 4 if ($packetID -ne $null -and $packetID -ne $packetIDTmp) { # Invalid multipacket packetID. PacketID does not match the multipacket set's packetID return } $packetID = $packetIDTmp # PacketCount # PacketNumber and PacketSize for newer Source Engines only if ($Engine -match '^Source$') { $packetCount = $buffer.GetByte() # 1 $packetNumber = $buffer.GetByte() # 1 $packetSize = $buffer.GetShort() # 2 }elseif ($Engine -match '^Goldsource$') { if ($cnt -eq 0) { $packetCount = $buffer.GetByte() # 1 }else { $Junk_0x12 = $buffer.GetByte() # 1 } } } # FF FF FF FF, Header, and Rule count in first packet if ($cnt -eq 0) { $Junk = $buffer.GetLong() $Header = $buffer.GetByte() $Rules_count = $buffer.GetShort() $Rules['Rules_count'] += $Rules_count } while ($buffer.HasMore()) { $rule = [ordered]@{ Name = if ($remainderString) { # Prepend the remainder of the previous tuncated packet to this first entry of current packet $remainderString + $buffer.GetString() } else { $buffer.GetString() } Value = $buffer.GetString() } $Rules['Rules'].Add( $rule ) > $null $remainderString = '' } $remainderString = $buffer.GetRemainingString() if ($remainderString -eq '') { break } $cnt++ } }catch { if ($rPack -eq $null) { throw } } } return $Rules }elseif ($requestBody -eq $A2A_PING) { # A2A_PING is no longer supported on Counter Strike: Source and Team Fortress 2 servers, and is considered a deprecated feature. See: https://developer.valvesoftware.com/wiki/Server_Queries#A2A_PING # A2A_PING request $pack = @(255,255,255,255) + $requestBody SendPacket $pack $rpack = ReceivePacket if (!$rPack.Length) { return } # Are we banned? $buffer = [SourceQueryBuffer]::New($rPack) $Junk = $buffer.GetLong() $Header = $buffer.GetByte() if ($Header -eq 0x6C) { # 'l' - Banned by the server. $Ping = [ordered]@{ Message = $buffer.GetString() Banned = $true } } $ping_response = $buffer.GetByte() $Ping = [ordered]@{ Success = $true } return $Ping } } function GetResponse ($pack) { $response = $enc.GetString( $pack[5..($pack.Length - 1)] ) $response } function Debug-Packet ($label, $pack) { if ($pack) { Write-Verbose "[$label]" #Write-Verbose "pack: $pack" Write-Verbose "pack: $( $pack | % { $_.ToString('X2').PadLeft(2) } )" Write-Verbose "pack: " Write-Verbose "$( $pack | % { if ($_ -eq 0x00) { "\".PadLeft(2) } else { [System.Text.Encoding]::Utf8.GetString($_).Trim().PadLeft(2) } } )" Write-Verbose "length: $($pack.Length)" Write-Verbose "" } } # Query $answer = GetQueryData $udpClient.Dispose() $answer }catch { if ($ErrorActionPreference -eq 'Stop') { throw }else { Write-Error -ErrorRecord $_ } } } |