public/SourceRcon.ps1
<# .SYNOPSIS Performs a Source Rcon. .DESCRIPTION Performs a Source Rcon. .PARAMETER Address DNS or IP address. .PARAMETER Port Port. .PARAMETER Password Rcon password. .PARAMETER Command Rcon command. .EXAMPLE SourceRcon -Address $address -Port $port -Password $rcon_password -Command 'status' .NOTES Source: https://developer.valvesoftware.com/wiki/Source Source Rcon: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol #> function SourceRcon { [CmdletBinding()] param( [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Address , [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [int]$Port , [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Password , [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Command ) try { # Determine the IP $Address = Resolve-DNS -Address $Address Write-Verbose "Sending SourceRcon to $Address`:$Port" $enc = [system.Text.Encoding]::UTF8 # Rcon props $SERVERDATA_AUTH = 3 $SERVERDATA_EXECCOMMAND = 2 $SERVERDATA_AUTH_RESPONSE = 2 $SERVERDATA_RESPONSE_VALUE = 0 $auth = 0 $packetID_Auth = 1 $packetID = 10 $packetID_MultipackDummy = $SERVERDATA_RESPONSE_VALUE # Set up TCP Socket $tcpClient = New-Object System.Net.Sockets.TcpClient $tcpClient.Client.SendTimeout = 500 $tcpClient.Client.ReceiveTimeout = 500 # Connect the TCP Socket (Sync) - Not using this because there's no socket timeout! <# $remoteEP = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($Address), $Port) $tcpClient.Connect($remoteEP) if (!$tcpClient.Connected) { Write-Host "Could not connect to remote host: $Address`:$Port" } #> # Connect the TCP Socket (Async yet sync) - Now there's a socket timeout $result = $tcpClient.BeginConnect([System.Net.IPAddress]::Parse($Address), $Port, $null, $null) $success = $result.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds(2)) if (!$success) { throw "Could not connect to remote host: $Address`:$Port" } if (! $tcpclient.Connected) { throw "Could not connect to remote host: $Address`:$Port" } # Set up Network stream [System.Net.Sockets.NetworkStream]$stream = $tcpClient.GetStream() $stream.ReadTimeout = 500 $stream.WriteTimeout = 500 function IntToBytes ([int]$integer) { [byte[]]$bytes = [BitConverter]::GetBytes($integer) if (![BitConverter]::IsLittleEndian) { [array]::Reverse($bytes) } $bytes } function BytesToInt32 ([byte[]]$bytes) { if ($bytes.Length -gt 0) { [BitConverter]::ToInt32($bytes, 0) } } function BuildPacket ([int]$ID, [int]$TYPE, [string]$BODY) { $pack = (IntToBytes $ID) + (IntToBytes $TYPE) + $enc.GetBytes($BODY) + 0 + 0 $pack = (IntToBytes $pack.Length) + $pack $pack } function SendPacket ([byte[]]$pack) { Debug-Packet $MyInvocation.MyCommand.Name $pack $stream.Write($pack, 0, $pack.Length) } function ReceivePacket ([int]$packetSize) { [byte[]]$pack = New-Object byte[] $packetSize $memStream = New-Object System.IO.MemoryStream $bytes = 0 do { try { $bytes = $stream.Read($pack, 0, $pack.Length) $memStream.Write($pack, 0, $bytes) Debug-Packet $MyInvocation.MyCommand.Name $pack if ($pack) { break } }catch { throw "Did not receive any response." } }while($bytes -gt 0) $memStream.Dispose() $pack } function ParsePacket ([byte[]]$pack) { if ($pack.Length -ge 10) { $IdBytes = $pack[0..3] $typeBytes = $pack[4..7] $bodyBytes = $pack[8..($pack.Length -1 -1)] # Ignore Null Character at 1) at Packet Empty String Terminator 2) end of Packet Body @{ Id = BytesToInt32 $IdBytes Type = BytesToInt32 $typeBytes Body = $enc.GetString($bodyBytes) IdBytes = $IdBytes TypeBytes = $typeBytes BodyBytes = $bodyBytes } } } function Auth { $pack = BuildPacket $packetID_Auth $SERVERDATA_AUTH $Password SendPacket $pack $emptyPack = ReceivePacket (4+10) $authPack = ReceivePacket (4+10) $ID = BytesToInt32 $authPack[4..7] $ID } # Send and receive (Synchronous) function SendReceive ([string]$Command) { $pack = BuildPacket $packetID $SERVERDATA_EXECCOMMAND $Command SendPacket $pack $answer = '' while ($true) { try { # Read the size of packet $rPack = ReceivePacket 4 if (!$rPack.Length) { return } $size = BytesToInt32 $rPack if ($size -eq 0) { # No more packets to read from socket. E.g. 'exit' break } # Now read the packet $rPack = ReceivePacket $size $response = ParsePacket $rPack if ($response['ID'] -eq $packetID_MultipackDummy) { # At the end of a multiple-packet response, the dummy empty packet is finally mirrored, followed by another RESPONSE_VALUE packet containing 0x0000 0001 0000 0000 in the packet body field. # See: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses $rPack = ReceivePacket 4 $size = BytesToInt32 $rPack $rPack = ReceivePacket $size $response = ParsePacket $rPack if ( $response['Body'] -eq $enc.GetString([byte[]]@(0x00, 0x01, 0x00, 0x00)) ) { Write-Verbose "End of multiple-packet response." break } } $answer += $response['Body'].Trim() if (!$dummyPacketSent) { # Always send one dummy empty packet right after the sending a first packet to determine whether we will get a multiple-packet response # See: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses Write-Verbose "Sending dummy empty packet." $pack = BuildPacket $packetID_MultipackDummy $SERVERDATA_RESPONSE_VALUE '' SendPacket $pack $dummyPacketSent = $true } }catch { # No more packets to read from socket break } } $answer } function Debug-Packet ([string]$label, [byte[]]$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 "" } } # Rcon $success = Auth if ($success -eq -1) { throw "Bad rcon password." }else { $auth = 1 # Send and receive (Sync) $answer = SendReceive $Command $packetID++ } $tcpClient.Dispose() $answer }catch { if ($ErrorActionPreference -eq 'Stop') { throw }else { Write-Error -ErrorRecord $_ } } } |