Ut99Tools.psm1
|
function Find-UtLanServers { <# .SYNOPSIS Find UT Servers on a Lan and write their response to the pipeline. .DESCRIPTION Mimics the behavior of the UT client when a user selects the "Find LAN games option" by sending out broadcast packets to ports 8777-8786 and reading the responses it recieves. .EXAMPLE PS C:\> Find-UtLanServers GameType QueryPort Address -------- --------- ------- ut 7778 192.168.0.6 Running the Cmdlet will find all servers that are listening on the correct port range and output the above result. .INPUTS .OUTPUTS PsCustomObject #> [CmdletBinding()] param () try { $udpClient = New-Object System.Net.Sockets.UdpClient $udpClient.EnableBroadcast = $true $udpClient.Client.ReceiveTimeout = 2000 $udpClient.Client.SendTimeout = 2000 $encoding = [System.Text.AsciiEncoding]::new() #fishing for servers on the network. [Byte[]] $sendQueryBytes = $encoding.GetBytes("REPORTQUERY"); 8777..8786 | ForEach-Object { Write-Verbose "Sending broadcast message REPORTQUERY to port $_" $udpClient.Send($sendQueryBytes, $sendQueryBytes.Length, [IPEndPoint]::new([IPAddress]::Broadcast, $_)) | Out-Null } while ($true) { Write-Verbose "Waiting for responses" $ServerEndpoint = [IPEndpoint]::new([ipaddress]::Any, 0) [Byte[]] $receiveBytes = $udpClient.Receive([ref]$ServerEndpoint) [string] $returnData = $encoding.GetString($receiveBytes); if ($returnData) { Write-Verbose "Response received" $output = [PSCustomObject]@{ GameType = $returnData.Split(' ')[0] QueryPort = $returnData.Split(' ')[1] Address = $ServerEndpoint.Address } $output | Write-Output } } } catch { if ($_.Exception.InnerException.ErrorCode -eq 10060) { Write-Verbose "Haven't heard anything for a while. Shutting the connection down. " } } finally { $udpClient.Close() $udpClient.Dispose() } } function Get-UtMasterServerEndpointList { <# .SYNOPSIS Retrieves Endpoints from Master Servers. .DESCRIPTION Retrieves a list of IP Endpoints from master servers. It's useful for testing if your UT99 game server is successfully announcing its presense to a master server. When a UT Game server is started it might say: UdpServerUplink: Master Server is master.mplayer.com:27900 UdpServerUplink: Port 7779 successfully bound. However, this just means that it was able to a) resolve the dns host name to an ip address and b) bind to port 7779 on the system. It does not mean the game server successfully announced its presense to that master server. Another problem is the UT99 Game client, will only show a list of servers in the browser that it managed to ping, so, you will not know if the problem is between the client and the game server, or the game server and the master server. This cmdlet allows you to test two things 1. The master server address and port number are responding to connections. 2. The list of IP Endpoints is retrievable and your game server's IP Endpoint is among them. .EXAMPLE PS C:\> Get-UtMasterServerEndpointList -Address utmaster.epicgames.com AddressFamily Address Port ------------- ------- ---- InterNetwork 147.135.23.65 7978 InterNetwork 216.155.140.138 7778 InterNetwork 213.230.216.2 8889 InterNetwork 85.14.229.240 7778 InterNetwork 5.9.21.239 8076 Providing only the required parameters will return the list of endpoints .EXAMPLE PS C:\> Get-UtMasterServerEndpointList -Address master.333networks.com AddressFamily Address Port ------------- ------- ---- InterNetwork 147.135.23.65 7978 InterNetwork 216.155.140.138 7778 The cmdlet solves the server's secure challenge with the GameSpy algorithm (gsmsalg), so any master server that speaks the GameSpy 'secure' protocol for Unreal Tournament (gamename 'ut') will return its endpoint list. .EXAMPLE PS C:\> Get-UtMasterServerEndpointList -Address master.oldunreal.com -GameName ut -GameKey Z5Nfb0 Supplying an explicit -GameName and -GameKey lets you query master servers for other GameSpy titles. The defaults ('ut' / 'Z5Nfb0') target Unreal Tournament, so they can be omitted for UT master servers. .EXAMPLE PS C:\> Get-UtMasterServerEndpointList -Address unreal.epicgames.com | where Address -eq '181.43.152.180' AddressFamily Address Port ------------- ------- ---- InterNetwork 181.43.152.180 8201 InterNetwork 181.43.152.180 8301 InterNetwork 181.43.152.180 8401 InterNetwork 181.43.152.180 7778 InterNetwork 181.43.152.180 8305 In this example, the endpoints are filtered to only show Endpoints where the ip address equals '181.43.152.180'. .INPUTS string .OUTPUTS object[] or System.Net.IPEndpoint .NOTES The master server's secure challenge is solved at runtime using the GameSpy master server algorithm (gsmsalg, enctype 0) keyed with the game key, so servers that issue different challenges are supported. Only the plain-text enctype 0 handshake used by Unreal Tournament master servers is implemented. .LINK https://github.com/RIKIKU/UT99-Tools #> [cmdletbinding()] param( # IP address or hostname of the master server [Parameter(Mandatory = $true)] [string] $Address, # Port number to connect to the server on. Default 28900 [Parameter(Mandatory = $false)] [int] $Port = 28900, # GameSpy game name announced in the list request. Default 'ut' (Unreal Tournament). [Parameter(Mandatory = $false)] [string] $GameName = 'ut', # GameSpy game key used to solve the master server's secure challenge. Default is the Unreal Tournament ('ut') key. [Parameter(Mandatory = $false)] [string] $GameKey = 'Z5Nfb0' ) begin { #region helpy helpertons function streamDataWaiter { <# .SYNOPSIS Waits for the stream.DataAvailable to be true or to timeout before moving on. #> [CmdletBinding()] param ( #The stream to check for data. [Parameter(Mandatory = $true)] [System.Net.Sockets.NetworkStream] $stream, #Effectively a timeout. Each loop waits 200 ms between each check. [Parameter(Mandatory = $false)] [int] $LoopLimit = 2000 ) $loop = 0 while ($stream.DataAvailable -eq $false -and $loop -lt $LoopLimit) { $loop++ Write-Verbose "Waiting for Server Response" Start-Sleep -m 200 } if ($loop -ge $LoopLimit) { Write-Error "Connection Timed-out waiting for response from server." -ErrorAction Stop } } function computeGsSecureResponse { <# .SYNOPSIS Solves a GameSpy 'secure' challenge (gsmsalg, enctype 0) using the game key. #> [CmdletBinding()] param ( # The challenge string sent by the master server (the value following '\secure\'). [Parameter(Mandatory = $true)] [string] $Challenge, # The GameSpy game key used to validate the challenge. [Parameter(Mandatory = $true)] [string] $GameKey ) # Maps a 6-bit value to a GameSpy base64-style output character (returns its ASCII code). function gsvalfunc([int]$reg) { if ($reg -lt 26) { return ($reg + 65) } if ($reg -lt 52) { return ($reg + 71) } if ($reg -lt 62) { return ($reg - 4) } if ($reg -eq 62) { return 43 } if ($reg -eq 63) { return 47 } return 0 } $encoder = [System.Text.Encoding]::ASCII $src = $encoder.GetBytes($Challenge) $key = $encoder.GetBytes($GameKey) # Key-scheduling: build the initial permutation from the game key. $enctmp = New-Object 'int[]' 256 for ($i = 0; $i -lt 256; $i++) { $enctmp[$i] = $i } $a = 0 for ($i = 0; $i -lt 256; $i++) { $a = ($a + $enctmp[$i] + $key[$i % $key.Length]) -band 0xff $swap = $enctmp[$a] $enctmp[$a] = $enctmp[$i] $enctmp[$i] = $swap } # Encrypt the challenge, then zero-pad the buffer to a multiple of three bytes. $tmp = New-Object 'int[]' 66 $a = 0 $b = 0 for ($i = 0; $i -lt $src.Length; $i++) { $a = ($a + $src[$i] + 1) -band 0xff $x = $enctmp[$a] $b = ($b + $x) -band 0xff $y = $enctmp[$b] $enctmp[$b] = $x $enctmp[$a] = $y $tmp[$i] = $src[$i] -bxor $enctmp[($x + $y) -band 0xff] } for ($size = $i; ($size % 3) -ne 0; $size++) { $tmp[$size] = 0 } # Emit the response as GameSpy base64 (4 output chars per 3 input bytes). $sb = [System.Text.StringBuilder]::new() for ($i = 0; $i -lt $size; $i += 3) { $x = $tmp[$i] $y = $tmp[$i + 1] $z = $tmp[$i + 2] [void]$sb.Append([char](gsvalfunc ($x -shr 2))) [void]$sb.Append([char](gsvalfunc ((($x -band 3) -shl 4) -bor ($y -shr 4)))) [void]$sb.Append([char](gsvalfunc ((($y -band 15) -shl 2) -bor ($z -shr 6)))) [void]$sb.Append([char](gsvalfunc ($z -band 63))) } $sb.ToString() } #endregion } process { try { try { Write-Verbose "Attempting connection to host" $tcpClient = New-Object System.Net.Sockets.TcpClient( $Address, $port ) $tcpClient.Client.ReceiveTimeout = 5000 $tcpClient.Client.SendTimeout = 5000 } catch { Write-Error -Message $_.Exception.Message -Exception $_.Exception -ErrorAction Stop } $stream = $tcpClient.GetStream( ) $writer = New-Object System.IO.StreamWriter( $stream ) $buffer = New-Object System.Byte[] $tcpClient.ReceiveBufferSize $encoding = New-Object System.Text.AsciiEncoding $IpString = [System.Text.StringBuilder]::new() #streamDataWaiter -stream $stream $read = $stream.Read( $buffer, 0, $tcpClient.ReceiveBufferSize ) $SecurityChallenge = $encoding.GetString( $buffer, 0, $read ) Write-Verbose "Security Challenge $SecurityChallenge" <# The master server opens with '\basic\\secure\<challenge>'. The <challenge> is solved with the GameSpy master server algorithm (gsmsalg, enctype 0) keyed with the game key, producing the '\validate\' token the server expects. See computeGsSecureResponse. #> $challengeMatch = [regex]::Match($SecurityChallenge, '\\secure\\([^\\]+)') if (-not $challengeMatch.Success) { Write-Error "Unexpected handshake from server. Expected a '\secure\' challenge but received: $SecurityChallenge" -ErrorAction Stop } $challengeValue = $challengeMatch.Groups[1].Value Write-Verbose "Parsed secure challenge '$challengeValue'" $validate = computeGsSecureResponse -Challenge $challengeValue -GameKey $GameKey Write-Verbose "Computed validate response '$validate'" Write-Verbose "Sending request for ip address list" $writer.WriteLine("\gamename\$GameName\location\0\validate\$validate\final\\list\gamename\$GameName\") $writer.Flush( ) #sometimes, this loop gets stuck here. I need to fix this. maybe with a timeout? #Adding the sleep above seemed to fix the issue, but it's not a good implementation. do { Write-Verbose "Receiving List of Endpoints" $read = $stream.Read( $buffer, 0, $tcpClient.ReceiveBufferSize ) $IpString.Append($encoding.GetString( $buffer, 0, $read )) | Out-Null while ($stream.DataAvailable -eq $false) { if ($IpString.tostring() -match 'final') { break } Write-Verbose "waiting for data" Start-Sleep -m 20 } }while ( $stream.DataAvailable ) Write-Verbose "Processing Ip Address List." $IpString = $IpString.ToString().Split('\final')[0] $IpString.Split('\ip\', [StringSplitOptions]::RemoveEmptyEntries).ForEach( { [IPEndpoint]::Parse($_) | Write-Output }) } finally { if ( $writer ) { $writer.Close( ) $writer.Dispose() } if ( $stream ) { $stream.Close( ) $stream.Dispose() } } } end {} } function Invoke-UtServerQuery { <# .SYNOPSIS Send a query to a UT Game Server. .DESCRIPTION Send a query message to a Game Server's query port (game port + 1). This will retrieve the type of information that's usually visible in the game "Find Internet Games window" and more. .EXAMPLE PS C:\> Find-UtLanServers | Invoke-UtServerQuery hostaddress : 192.168.0.6 hostport : 7777 worldlog : false maptitle : Frigate maxplayers : 16 gamemode : openplaying gametype : Assault mapname : AS-Frigate hostname : Testing UT Server In Docker wantworldlog : false minnetver : 432 queryid : 5.1 numplayers : 1 gamever : 451 In this example, Find-UtLanServers locates a server on the LAN and Invoke-UtServerQuery performs the info query on it.. .EXAMPLE PS C:\> Get-UtMasterServerEndpointList -Address utmaster.epicgames.com | where Address -eq '181.43.152.180' | Invoke-UtServerQuery -QueryType rules | select hostname,mapname,numplayers hostname mapname numplayers -------- ------- ---------- [Ragnarok] Heimdall - DM 1v1 - ip2 CTF-Lucius 0 [Ragnarok] Odin - [Pug] ip3 CTF-Lucius 0 [Ragnarok] Thor - [Pug] ip4 CTF-Lucius 0 [Ragnarok] Tyr - CTF/DM Publico -… CTF-Novem… 0 [Ragnarok] Loki - [Pug] ip5 OldCo… CTF-Incin… 0 In this example, we query the master server for a list of game servers, then filter the list to only IP addresses that match '181.43.152.180' and query only those servers. Then we filter the output to only show hostname, mapname and numplayers. .INPUTS String, Int .OUTPUTS PSCustomObject .NOTES General notes #> [CmdletBinding()] param ( #Address of the UT server to Query. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Address, # The Game Server's query port (ServerPort + 1) [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('Port')] [int] $QueryPort, # Query type can be one of the following values: info, rules, players, 'status', 'echo', 'level_property', 'player_property' [Parameter(Mandatory = $false)] [ValidateSet( 'info', 'rules', 'players', 'status', 'echo', 'level_property', 'player_property', IgnoreCase = $false )] [string] $QueryType = 'info' ) begin { } process { try { $respondentEndpoint = [IPEndpoint]::new([ipaddress]::Parse($Address), $QueryPort) $UtServerUtpClient = [System.Net.Sockets.UdpClient]::new($respondentEndpoint.Port) $UtServerUtpClient.Client.SendTimeout = 2000 $UtServerUtpClient.Client.ReceiveTimeout = 5000 Write-Verbose "Connecting to server" $UtServerUtpClient.Connect($respondentEndpoint) $encoding = [System.Text.AsciiEncoding]::new() $query = '\{0}\' -f $QueryType [Byte[]] $sendInfoBytes = $encoding.GetBytes($query); Write-Verbose "Sending query" $UtServerUtpClient.Send($sendInfoBytes, $sendInfoBytes.Length) | Out-Null Write-Verbose "Attempting to receive response" [Byte[]] $receiveBytes = $UtServerUtpClient.Receive([ref]$respondentEndpoint) $returnData = $encoding.GetString($receiveBytes) } catch { if ($_.Exception.InnerException.ErrorCode -eq 10060) { Write-Verbose "Host $($respondentEndpoint.ToString()) Didn't respond." } } finally { if ($UtServerUtpClient) { $UtServerUtpClient.Close() $UtServerUtpClient.Dispose() } } if ($returnData) { <# The response from the server is something like: \hostname\UT Server In Docker\hostport\7777\maptitle\HiSpeed... It's essentially a key value pair in string format, so we split it into separate objects and order them as a hash table. The hash is converted into a pscustomobject for that powershell feeling. #> Write-Verbose "Server Response: $returnData " #remove leading \ or \\ depending. Write-Verbose "Processing response" $returnData = $returnData.Substring(1, $returnData.Length - 1) if ($returnData.Substring(0, 2) -eq '\\') { $returnData = $returnData.Substring(2, $returnData.Length - 2) } $splitPath = $returnData.Split('\') $hash = @{} try { for ($i = 0; $i -lt $splitPath.Count; $i++) { $hash[$splitPath[$i]] = $splitPath[$i + 1] $i++ } #final isn't used for anything. $hash.Remove('final') #Adding host address to output to make it more useful. $hash['hostaddress'] = $respondentEndpoint.Address [PSCustomObject]$hash | Write-Output } catch { Write-Error " There was a problem processing the response from the server." } } } end { } } |