MineStat.psm1
### # MineStat.psm1 # Copyright (C) 2020-2024 Ajoro and MineStat contributors. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ### <# .SYNOPSIS MineStat is a Minecraft server connection status checker. .EXAMPLE MineStat -Address 'minecraft.frag.land' -Port 25565 -Timeout 10 .LINK https://github.com/FragLand/minestat #> function MineStat { [CmdletBinding()] param ( # Addresss (domain or IP-address) of the server to connect to. # Input as str or str[] [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Server', 'Host', 'IP')] [string[]]$Address = "localhost", # Port of the server to connect to. [uint16]$Port = 25565, # SlpProtocol to use # Possible values: "BedrockRaknet", "Json", "Extendedlegacy", "Legacy", "Beta" # Can combine protocols to check more. # Defaults to check: "Json", "Extendedlegacy", "Legacy", "Beta" [ValidateSet("BedrockRaknet", "Json", "Extendedlegacy", "Legacy", "Beta", "Query")] [string[]]$Protocol = 31, # The time in seconds, after which a connection is timed out. [int]$Timeout = 5, [switch]$IgnoreSRV = $false ) enum ConnStatus { # The specified SLP connection succeeded (Request & response parsing OK) Success = 1 # The connection attempt failed for an unknown reason. Unknown = 0 # If a connection was made but the server reponded with a invalid response for this protocol InvalidResponse = -1 # The connection timed out. (Server under too much load? Firewall rules OK?) Timeout = -2 # The socket to the server could not be established. Server offline, wrong hostname or port? ConnFail = -3 } [Flags()] enum SlpProtocol { BedrockRaknet = 32 Query = 16 Json = 8 ExtendedLegacy = 4 Legacy = 2 Beta = 1 Unknown = 0 } $ModuleInfos = Import-PowerShellDataFile -Path "$PsScriptRoot\MineStat.psd1" Write-Verbose "MineStat version: $($ModuleInfos.ModuleVersion.ToString())" try { function New-ServerStatus { param ( [string]$Address, [uint16]$Port, [int]$Timeout, [SlpProtocol]$Protocol, [switch]$IgnoreSRV ) $split = $Address -split ":" $port = if ($split.Count -gt 1) { $split[1] } else { $Port } $protocol = if ($split.Count -eq 3) { $split[2] } else { $Protocol } [ServerStatus]::new($split[0], $port, $Timeout, $protocol, $IgnoreSRV) } $returnArray = foreach ($Addr in $Address) { New-ServerStatus -Address $Addr -Port $Port -Timeout $Timeout -Protocol $Protocol -IgnoreSRV:$IgnoreSRV } return $returnArray } catch { throw $_ } class ServerStatus { [string]$address = "localhost" [uint16]$port = 25565 [bool]$online = $false [string]$version [string]$formatted_motd [int]$current_players = -1 [int]$max_players = -1 [int]$latency = -1 [SlpProtocol]$slp_protocol # hidden values make a better looking return when running from the console hidden [int]$timeout hidden [string]$favicon hidden [string]$gamemode hidden [string[]]$playerList hidden [string]$motd hidden [string]$map hidden [string]$plugins hidden [string]$stripped_motd hidden [string]$connection_status = [ConnStatus]::Unknown ServerStatus($address, $port, $timeout, [SlpProtocol]$queryprotocol, $ignoresrv) { try { $resolved = Resolve-DnsName -type srv _minecraft._tcp.$address -ErrorAction Stop if ($ignoresrv -or $resolved.type -ne "SRV") { throw } $this.address = $resolved[0].NameTarget $this.port = $resolved[0].port Write-Verbose ("Found {0}:{1}" -f $this.address, $this.port) } catch { $this.address = $address $this.port = $port } $this.timeout = $timeout Write-Verbose "Checking SlpProtocol: $queryprotocol" # Minecraft Bedrock/Pocket/Education Edition (MCPE/MCEE) if ($queryprotocol.HasFlag([SlpProtocol]::BedrockRaknet)) { $this.connection_status = $this.RequestWithRaknetProtocol() Write-Verbose "BedrockRaknet - $($this.connection_status.ToString())" } # Minecraft 1.4 & 1.5 (legacy SLP) if ($queryprotocol.HasFlag([SlpProtocol]::Legacy) -and $this.connection_status -notin [ConnStatus]::ConnFail, [ConnStatus]::Success) { $this.connection_status = $this.RequestWithLegacyProtocol() Write-Verbose "Legacy - $($this.connection_status.ToString())" } # Minecraft Beta 1.8 to Release 1.3 (beta SLP) if ($queryprotocol.HasFlag([SlpProtocol]::Beta) -and $this.connection_status -notin [ConnStatus]::ConnFail, [ConnStatus]::Success) { $this.connection_status = $this.RequestWithBetaProtocol() Write-Verbose "Beta - $($this.connection_status.ToString())" } # Minecraft 1.6 (extended legacy SLP) if ($queryprotocol.HasFlag([SlpProtocol]::ExtendedLegacy) -and $this.connection_status -notin [ConnStatus]::ConnFail) { $result = $this.RequestWithExtendedLegacyProtocol() if ($result -ge $this.connection_status) { $this.connection_status = $result } Write-Verbose "ExtendedLegacy - $result" } # Minecraft 1.7+ (JSON SLP) if ($queryprotocol.HasFlag([SlpProtocol]::Json) -and $this.connection_status -notin [ConnStatus]::ConnFail) { $result = $this.RequestWithJsonProtocol() if ($result -ge $this.connection_status) { $this.connection_status = $result } Write-Verbose "Json - $result" } # Minecraft Query/GameSpot4/UT3 protocol. if ($queryprotocol.HasFlag([SlpProtocol]::Query) -and $this.connection_status -notin [ConnStatus]::ConnFail) { $this.connection_status = $this.FullstatQuery() Write-Verbose "Query - $($this.connection_status.ToString())" } } [string[]] generateMotds($rawmotd) { function strip_motd($rawmotd) { # Function for stripping all formatting codes from a motd. $stripped_motd = "" if ($rawmotd.gettype().name -eq "string") { $stripped_motd = $rawmotd -split "(?:\\u00A7|$([char]0x00A7))+[a-zA-Z0-9]" -join "" } else { $stripped_motd = $rawmotd.text if ($rawmotd.extra) { foreach ($sub in $rawmotd.extra) { $stripped_motd += strip_motd($sub) } } if ($stripped_motd -match [char]0x00A7) { $stripped_motd = strip_motd($stripped_motd) } } return $stripped_motd } function format_motd($rawmotd) { # Function for formating all formatting codes as escaped unicode characters from motd. $formatcodes = @{ "$([char]0x00A7)0" = "$([char]27)[0;30m" # Black "$([char]0x00A7)1" = "$([char]27)[0;34m" # DarkBlue "$([char]0x00A7)2" = "$([char]27)[0;32m" # DarkGreen "$([char]0x00A7)3" = "$([char]27)[0;36m" # DarkCyan (Dark aqua) "$([char]0x00A7)4" = "$([char]27)[0;31m" # DarkRed "$([char]0x00A7)5" = "$([char]27)[0;35m" # DarkMagenta (Dark purple) "$([char]0x00A7)6" = "$([char]27)[0;33m" # DarkYellow (Gold) "$([char]0x00A7)7" = "$([char]27)[0;37m" # Gray "$([char]0x00A7)8" = "$([char]27)[0;90m" # DarkGray "$([char]0x00A7)9" = "$([char]27)[0;94m" # Blue "$([char]0x00A7)a" = "$([char]27)[0;92m" # Green "$([char]0x00A7)b" = "$([char]27)[0;96m" # Cyan (Aqua) "$([char]0x00A7)c" = "$([char]27)[0;91m" # Red "$([char]0x00A7)d" = "$([char]27)[0;95m" # Magenta (Light purple) "$([char]0x00A7)e" = "$([char]27)[0;93m" # Yellow "$([char]0x00A7)f" = "$([char]27)[0;97m" # White "$([char]0x00A7)g" = "$([char]27)[0;93m" # Yellow (Minecoin Gold) "$([char]0x00A7)k" = "$([char]27)[8m" # obfuscated "$([char]0x00A7)l" = "$([char]27)[1m" # bold "$([char]0x00A7)m" = "$([char]27)[9m" # strikethrough "$([char]0x00A7)n" = "$([char]27)[4m" # underline "$([char]0x00A7)o" = "$([char]27)[3m" # italic "$([char]0x00A7)r" = "$([char]27)[0m" # reset formating } $formats = @{ "obfuscated" = "$([char]27)[8m" "bold" = "$([char]27)[1m" "strikethrough" = "$([char]27)[9m" "underline" = "$([char]27)[4m" "italic" = "$([char]27)[3m" "reset" = "$([char]27)[0m" } $colorcodes = @{ black = "$([char]27)[0;30m" dark_blue = "$([char]27)[0;34m" dark_green = "$([char]27)[0;32m" dark_aqua = "$([char]27)[0;36m" dark_red = "$([char]27)[0;31m" dark_purple = "$([char]27)[0;35m" gold = "$([char]27)[0;33m" gray = "$([char]27)[0;37m" dark_gray = "$([char]27)[0;90m" blue = "$([char]27)[0;94m" green = "$([char]27)[0;92m" aqua = "$([char]27)[0;96m" red = "$([char]27)[0;91m" light_purple = "$([char]27)[0;95m" yellow = "$([char]27)[0;93m" white = "$([char]27)[0;97m" minecoin_gold = "$([char]27)[0;93m" # Yellow (Minecoin Gold) } $formatted_motd = "" if ($rawmotd.gettype().name -eq "string") { $rawmotd = $rawmotd -replace "\\u00A7", "$([char]0x00A7)" foreach ($format in ($rawmotd -split "($([char]0x00A7)+[a-zA-Z0-9])")) { if ($format -in $formatcodes.Keys) { $formatted_motd += $formatcodes.$format } if ($format -match [char]0x00A7) { continue } else { $formatted_motd += $format } } return $formatted_motd + $formats.reset } else { foreach ($entry in $rawmotd) { $formatted_motd += $formats.reset if ($entry.keys.length -ge 2 -and $entry.text) { if ($entry.color) { $formatted_motd += $colorcodes.($entry.color) } foreach ($option in $formats.Keys) { if ($option -in $entry.keys) { $formatted_motd += $formats.$option } } } $formatted_motd += $entry.text if ($entry.extra) { format_motd($entry.extra) } } if ($formatted_motd -match [char]0x00A7) { $formatted_motd = format_motd($formatted_motd) } return $formatted_motd + $formats.reset } } $stripped = strip_motd($rawmotd) $formatted = format_motd($rawmotd) return $stripped, $formatted } [ConnStatus] FullstatQuery() { <# Method for querying a Minecraft Java server using the fullstat Query / GameSpot4 / UT3 protocol. Needs to be enabled on the Minecraft server using: "enable-query=true" in the servers "server.properties" file. This method ONLY supports full stat querys. Documentation for this protocol: https://wiki.vg/Query #> $sock = New-Object System.Net.Sockets.UdpClient $sock.Client.ReceiveTimeout = $this.timeout * 1000 $sock.Client.SendTimeout = $this.timeout * 1000 $stopwatch = New-Object System.Diagnostics.Stopwatch $stopwatch.Start(); try { $sock.Connect($this.address, $this.port) } catch { $this.latency = -1 $stopwatch.Stop() return [ConnStatus]::ConnFail } $stopwatch.Stop() if ($this.latency -eq -1) { $this.latency = $stopwatch.ElapsedMilliseconds } $querymagic = [byte[]]@(254, 253) # b"\xFE\xFD" $handshake_packettype = [byte[]]@(9) $stat_packettype = [byte[]]@(0) $session_id_int = Get-Random -Minimum 0 -Maximum 2147483647 $session_id_bytes = [BitConverter]::GetBytes($session_id_int -band 0x0F0F0F0F) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($session_id_bytes); } $handshake_packet = $querymagic + $handshake_packettype + $session_id_bytes try { $sock.Send($handshake_packet, $handshake_packet.Length) $handshake_res = $sock.Receive([ref]$null) $challenge_token = $handshake_res[5..$($handshake_res.Length - 1)] $challenge_token_int = [int][System.Text.Encoding]::UTF8.GetString($challenge_token) $challenge_token_bytes = [BitConverter]::GetBytes($challenge_token_int) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($challenge_token_bytes); } $req_packet = $querymagic + $stat_packettype + $session_id_bytes + $challenge_token_bytes + [byte[]](0, 0, 0, 0) $sock.Send($req_packet, $req_packet.Length) $raw_res = $sock.Receive([ref]$null) $sock.Close() return $this.ParseFullstatQuery($raw_res[($session_id_bytes.Length + 1)..($raw_res.Length - 1)]) } catch [System.Net.Sockets.SocketException] { if ($_.Exception.Message -match "timed out") { return [ConnStatus]::Timeout } else { return [ConnStatus]::Unknown } } finally { $sock.Close() } } hidden [ConnStatus] ParseFullstatQuery([byte[]]$raw_res) { <# Helper method for parsing the reponse from a query request. See https://wiki.vg/Query for details. This implementation does not parse every value returned by the query protocol. #> try { # Remove unnecessary padding $res = $raw_res[11..($raw_res.Length - 1)] # Split stats from players $raw_stats, $raw_players = [Text.Encoding]::UTF8.GetString($res) -split [Text.Encoding]::UTF8.GetString(@(0x00, 0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00)) # Split stat keys and values into individual elements and remove unnecessary padding $stat_list = $raw_stats -split "`0" # Move keys and values into a dictionary, the keys are also decoded $stats = @{} for ($i = 0; $i -lt $stat_list.Length; $i += 2) { $key = $stat_list[$i] $value = $stat_list[$i + 1] $stats[$key] = $value } # Extract motd (hostname) or MOTD $this.motd = $null if ($stats.ContainsKey("hostname")) { $this.motd = $stats["hostname"] } elseif ($stats.ContainsKey("MOTD")) { $this.motd = $stats["MOTD"] } $this.stripped_motd, $this.formatted_motd = $this.generateMotds($this.motd) # Extract the server's Minecraft version $this.version = $null if ($stats.ContainsKey("version")) { $this.version = $stats["version"] } # Extract list of plugins $this.plugins = @() if ($stats.ContainsKey("plugins")) { $raw_plugins = $stats["plugins"] if ($raw_plugins -ne "") { # The plugins are separated by " ;" $this.plugins = $raw_plugins -split " ;" # There may be information about the server software in the first plugin element if ($this.plugins[0] -match ":") { $this.version, $this.plugins[0] = $this.plugins[0] -split ": ", 2 } } } # Extract the name of the map the server is running on if ($stats.ContainsKey("map")) { $this.map = $stats["map"] } # Extract number of online and maximum allowed players $this.current_players = 0 $this.max_players = 0 if ($stats.ContainsKey("numplayers")) { $this.current_players = [int]$stats["numplayers"] $this.max_players = [int]$stats["maxplayers"] } $this.playerList = $raw_players.TrimEnd("`0") -split "`0" $this.Slp_Protocol = "Query"; $this.online = $true; $this.Gamemode = $stats.gametype return [ConnStatus]::Success } catch { return [ConnStatus]::Unknown } } [ConnStatus] RequestWithRaknetProtocol() { <# Method for querying a Bedrock server (Minecraft PE, Windows 10 or Education Edition). The protocol is based on the RakNet protocol. See https://wiki.vg/Raknet_Protocol#Unconnected_Ping Note: This method currently works as if the connection is handled via TCP (as if no packet loss might occur). Packet loss handling should be implemented (resending). #> function readbytestream([System.Collections.Generic.Queue[byte]]$que, [int]$count) { $resultBuffer = New-Object System.Collections.Generic.List[byte] for ($i = 0; $i -lt $count; $i++) { $resultBuffer.Add($que.Dequeue()) } return $resultBuffer.ToArray() } $sock = New-Object System.Net.Sockets.UdpClient $sock.Client.ReceiveTimeout = $this.timeout * 1000 $sock.Client.SendTimeout = $this.timeout * 1000 $stopwatch = New-Object System.Diagnostics.Stopwatch $stopwatch.Start(); try { $sock.Connect($this.address, $this.port) } catch { $this.latency = -1 $stopwatch.Stop() return [ConnStatus]::ConnFail } $stopwatch.Stop() if ($this.latency -eq -1) { $this.latency = $stopwatch.ElapsedMilliseconds } [byte[]]$raknetMagic = @(0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78) [System.Collections.Generic.List[byte]]$raknetPingHandshakePacket = 0x01 $unixtime = [System.BitConverter]::GetBytes([DateTimeOffset]::Now.ToUnixTimemilliseconds()) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($unixtime); } $raknetPingHandshakePacket.AddRange($unixtime) $raknetPingHandshakePacket.AddRange($raknetmagic) $raknetPingHandshakePacket.AddRange([System.BitConverter]::GetBytes([Int64]0x02)) $sendlen = $sock.Send($raknetPingHandshakePacket.ToArray(), $raknetPingHandshakePacket.Count) if ($sendlen -ne $raknetPingHandshakePacket.Count) { return [ConnStatus]::Unknown } try { [System.Collections.Generic.Queue[byte]]$response = $sock.Receive([ref]$null) if ($response.Dequeue() -ne 0x1c) { return [ConnStatus]::InvalidResponse } # responseTimeStamp (never used) [System.BitConverter]::ToInt64((readbytestream $response 8), 0) # responseServerGUID (never used) [System.BitConverter]::ToInt64((readbytestream $response 8), 0) [byte[]]$responseMagic = readbytestream $response 16 if ($null -ne (Compare-Object $responseMagic $raknetMagic -CaseSensitive)) { return [ConnStatus]::Unknown } if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($response); } # responseIdStringLength (never used) [System.BitConverter]::ToUInt16((readbytestream $response 2), 0) $temp = readbytestream $response $response.Count $responseIdString = [System.Text.Encoding]::UTF8.GetString($temp) } catch { $this.latency = -1 $stopwatch.Stop() return [ConnStatus]::Timeout } finally { $sock.Close() } return $this.ParseBedrockPayload($responseIdString) } hidden [ConnStatus] ParseBedrockPayload([string]$payload) { $values = $payload -split ";" $keys = @("edition", "motd_1", "protocol_version", "version", "current_players", "max_players", "server_uid", "motd_2", "gamemode", "gamemode_numeric", "port_ipv4", "port_ipv6") $payload_obj = @{} for ($i = 0; $i -lt $keys.Count; $i++) { $payload_obj.Add($keys[$i], $values[$i]) } $this.Slp_Protocol = "BedrockRaknet"; $this.online = $true; $this.current_players = $payload_obj.current_players $this.max_players = $payload_obj.max_players $this.version = @($payload_obj.version, "($($payload_obj.edition))") -join " " $this.motd = $payload_obj.motd_1 + "`n" + $payload_obj.motd_2 $this.stripped_motd, $this.formatted_motd = $this.generateMotds($this.motd) $this.Gamemode = $payload_obj.gamemode return [ConnStatus]::Success } [ConnStatus] RequestWithJsonProtocol() { <# Method for querying a modern (MC Java >= 1.7) server with the SLP protocol. This protocol is based on encoded JSON, see the documentation at wiki.vg below for a full packet description. See https://wiki.vg/Server_List_Ping#Current #> function WriteLeb128([int]$value) { [System.Collections.Generic.List[byte]]$byteList = @() if ($value -eq -1) { [uint32] $actual = [uint32]"0xffffffff" } else { [uint32] $actual = [uint32]$value } do { [byte]$temp = $actual -band 127 $actual = $actual -shr 7 if ($actual -ne 0) { $temp = $temp -bor 128 } $byteList.Add($temp) } while ($actual -ne 0) return $byteList.ToArray() } function WriteLeb128Stream([System.Net.Sockets.NetworkStream]$stream, [int] $value) { if ($value -eq -1) { [uint32] $actual = [uint32]"0xffffffff" } else { [uint32] $actual = [uint32]$value } do { [byte]$temp = $actual -band 127 $actual = $actual -shr 7 if ($actual -ne 0) { $temp = $temp -bor 128 } $stream.WriteByte($temp) } while ($actual -ne 0) } function ReadLeb128Stream([System.Net.Sockets.NetworkStream]$stream) { $numRead = 0 $result = 0 do { [int] $r = $stream.ReadByte() if ($r -eq -1) { break } [byte]$read = $r [int] $value = $read -band 127 $result = $result -bor ($value -shl (7 * $numRead)) $numRead++ if ($numread -gt 5) { throw "VarInt is too big." } } while ( ($read -band 128) -ne 0 ) if ($numRead -eq 0) { throw "Unexpected end of VarInt stream." } return $result } $tcpclient = New-Object System.Net.Sockets.tcpclient $tcpclient.ReceiveTimeout = $this.Timeout * 1000 $tcpclient.SendTimeout = $this.Timeout * 1000 $stopwatch = New-Object System.Diagnostics.Stopwatch $stopwatch.Start(); $result = $tcpclient.BeginConnect($this.Address, $this.Port, $null, $null) $isResponsive = $result.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($this.Timeout)) if (-not $isResponsive) { $this.latency = -1 return [ConnStatus]::Timeout } try { $tcpclient.EndConnect($result) } catch [System.Net.Sockets.SocketException] { return [ConnStatus]::ConnFail } $stopwatch.Stop() if ($this.latency -eq -1) { $this.latency = $stopwatch.ElapsedMilliseconds } $stream = $tcpclient.GetStream() [System.Collections.Generic.List[byte]]$jsonPingHandshakePacket = 0x00 $jsonPingHandshakePacket.AddRange([byte[]] (WriteLeb128 -1)) $serverAddr = [System.Text.Encoding]::UTF8.GetBytes($this.Address) $jsonPingHandshakePacket.AddRange([byte[]] (WriteLeb128 $serverAddr.Length)) $jsonPingHandshakePacket.AddRange($serverAddr) $serverPort = [System.BitConverter]::GetBytes($this.Port) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($serverPort); } $jsonPingHandshakePacket.AddRange($serverPort) $jsonPingHandshakePacket.AddRange([byte[]] (WriteLeb128 1)) $jsonPingHandshakePacket.InsertRange(0, [byte[]] (WriteLeb128 $jsonPingHandshakePacket.Count)) try { $stream.Write($jsonPingHandshakePacket.ToArray() , 0, $jsonPingHandshakePacket.Count) WriteLeb128stream $stream 1 $stream.WriteByte(0x00) $responseSize = ReadLeb128Stream $stream } catch { return [ConnStatus]::Unknown } if ($responseSize -lt 3) { return [ConnStatus]::InvalidResponse } $responsePacketId = ReadLeb128Stream $stream if ($responsePacketId -ne 0x00) { return [ConnStatus]::InvalidResponse } $responsePayloadLength = ReadLeb128Stream $stream $responsePayload = $this.NetStreamReadExact($stream, $responsePayloadLength) return $this.ParseJsonProtocolPayload($responsePayload) } hidden [ConnStatus] ParseJsonProtocolPayload([byte[]]$rawPayload) { # Adds support for powershell version 5 since it dosn't support -Ashashtable tag on convertfrom-json # Thanks to Adam Bertram # https://4sysops.com/archives/convert-json-to-a-powershell-hash-table/ function ConvertTo-Hashtable { [CmdletBinding()] [OutputType('hashtable')] param ( [Parameter(ValueFromPipeline)] $InputObject ) process { ## Return null if the input is null. This can happen when calling the function ## recursively and a property is null if ($null -eq $InputObject) { return $null } ## Check if the input is an array or collection. If so, we also need to convert ## those types into hash tables as well. This function will convert all child ## objects into hash tables (if applicable) if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-Hashtable -InputObject $object } ) ## Return the array but don't enumerate it because the object may be pretty complex Write-Output -InputObject $collection -NoEnumerate } elseif ($InputObject -is [psobject]) { ## If the object has properties that need enumeration ## Convert it to its own hash table and return it $hash = @{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value } $hash } else { ## If the object isn't an array, collection, or other object, it's already a hash table ## So just return it. $InputObject } } } try { $payload_obj = ConvertFrom-Json ([System.Text.Encoding]::UTF8.GetString($rawPayload)) | ConvertTo-Hashtable } catch { return [ConnStatus]::InvalidResponse } $this.version = $payload_obj.version.name $descriptionElement = $payload_obj.description if ($null -ne $descriptionElement -and $descriptionElement.GetType().name -eq "string") { $this.motd = $descriptionElement } else { $this.motd = ConvertTo-Json $descriptionElement } $this.stripped_motd, $this.formatted_motd = $this.generateMotds($descriptionElement) $playerSampleElement = $payload_obj.players.sample if ($null -ne $playerSampleElement -and $playerSampleElement.GetType().BaseType.Name -eq "array") { $this.PlayerList = $playerSampleElement.name } if ($null -eq $this.version -or $null -eq $this.motd) { return [ConnStatus]::InvalidResponse } $this.Favicon = $payload_obj.favicon # $this.Protocol = $payload_obj.version.protocol; $this.max_players = $payload_obj.players.max; $this.current_players = $payload_obj.players.online; $this.Slp_Protocol = "Json"; $this.online = $true return [ConnStatus]::Success } [ConnStatus] RequestWithExtendedLegacyProtocol() { <# Minecraft 1.6 SLP query, extended legacy ping protocol. All modern servers are currently backwards compatible with this protocol. See https://wiki.vg/Server_List_Ping#1.6 #> $tcpclient = New-Object System.Net.Sockets.tcpclient $tcpclient.ReceiveTimeout = $this.Timeout * 1000 $tcpclient.SendTimeout = $this.Timeout * 1000 $stopwatch = New-Object System.Diagnostics.Stopwatch $stopwatch.Start(); $result = $tcpclient.BeginConnect($this.Address, $this.Port, $null, $null) $isResponsive = $result.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($this.Timeout)) if (-not $isResponsive) { $this.latency = -1 return [ConnStatus]::Timeout } try { $tcpclient.EndConnect($result) } catch [System.Net.Sockets.SocketException] { return [ConnStatus]::ConnFail } $stopwatch.Stop() if ($this.latency -eq -1) { $this.latency = $stopwatch.ElapsedMilliseconds } $stream = $tcpclient.GetStream() [System.Collections.Generic.List[byte]]$extlegacyPingPacket = @(0xFE, 0x01, 0xFA, 0x00, 0x0B) $extlegacyPingPacket.AddRange([System.Text.Encoding]::BigEndianUnicode.GetBytes("MC|PingHost")) $reqByteLen = [System.BitConverter]::GetBytes([Int16](7 + $this.Address.Length * 2)) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($reqByteLen); } $extlegacyPingPacket.AddRange($reqByteLen) $extlegacyPingPacket.Add(0x4A) $addressLen = [System.BitConverter]::GetBytes([Int16]$this.Address.Length) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($addressLen); } $extlegacyPingPacket.AddRange($addressLen) $extLegacyPingPacket.AddRange([System.Text.Encoding]::BigEndianUnicode.GetBytes($this.Address)) $portbytes = [System.BitConverter]::GetBytes([int]$this.Port) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($portbytes); } $extlegacyPingPacket.AddRange($portbytes) $stream.Write($extLegacyPingPacket.ToArray(), 0, $extLegacyPingPacket.Count); try { [byte[]] $responsePacketHeader = $this.NetStreamReadExact($stream, 3) } catch { return [ConnStatus]::Unknown } if ($responsePacketHeader[0] -ne 0xFF) { return [ConnStatus]::InvalidResponse } $responsePacketHeader $payloadLengthRaw = [System.Byte[]]::CreateInstance([System.Byte], $responsePacketHeader.Length - 1) [array]::Copy($responsePacketHeader, 1, $payloadLengthRaw, 0, ($responsePacketHeader.Length - 1)) if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($payloadLengthRaw); } $payloadLength = [System.BitConverter]::ToUInt16($payloadLengthRaw, 0) [byte[]]$payload = $this.NetStreamReadExact($stream, ($payloadLength * 2)) return $this.ParseLegacyProtocol($payload, "ExtendedLegacy") } [ConnStatus] RequestWithLegacyProtocol() { <# Minecraft 1.4-1.5 SLP query, server response contains more info than beta SLP See https://wiki.vg/Server_List_Ping#1.4_to_1.5 #> $tcpclient = New-Object System.Net.Sockets.tcpclient $tcpclient.ReceiveTimeout = $this.Timeout * 1000 $tcpclient.SendTimeout = $this.Timeout * 1000 $stopwatch = New-Object System.Diagnostics.Stopwatch $stopwatch.Start(); $result = $tcpclient.BeginConnect($this.Address, $this.Port, $null, $null) $isResponsive = $result.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($this.Timeout)) if (-not $isResponsive) { $this.latency = -1 return [ConnStatus]::Timeout } try { $tcpclient.EndConnect($result) } catch [System.Net.Sockets.SocketException] { return [ConnStatus]::ConnFail } $stopwatch.Stop() if ($this.latency -eq -1) { $this.latency = $stopwatch.ElapsedMilliseconds } $stream = $tcpclient.GetStream() [byte[]] $legacyPingPacket = 0xFE, 0x01 try { $stream.Write($legacyPingPacket, 0, $legacyPingPacket.Length); [byte[]] $responsePacketHeader = $this.NetStreamReadExact($stream, 3) } catch { return [ConnStatus]::Unknown } if ($responsePacketHeader[0] -ne 0xFF) { return [ConnStatus]::InvalidResponse } if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($responsePacketHeader); } $payloadLength = [System.BitConverter]::ToUInt16($responsePacketHeader, 0) if ($payloadLength -lt 3) { return [ConnStatus]::InvalidResponse } [byte[]]$payload = $this.NetStreamReadExact($stream , ($payloadLength * 2)) return $this.ParseLegacyProtocol($payload, "Legacy") } hidden [ConnStatus] ParseLegacyProtocol([byte[]]$rawPayload, [SlpProtocol]$SlpProtocol) { $payloadString = [System.Text.Encoding]::BigEndianUnicode.GetString($rawPayload, 0, $rawPayload.Length) $payloadArray = $payloadString.Split([char]0x0000) if ($payloadArray.Length -ne 6) { return [ConnStatus]::InvalidResponse } $this.Version = $payloadArray[2] $this.max_players = $payloadArray[5] $this.current_players = $payloadArray[4] $this.motd = $payloadArray[3] $this.stripped_motd, $this.formatted_motd = $this.generateMotds($this.motd) $this.Slp_Protocol = $SlpProtocol $this.online = $true return [ConnStatus]::Success } [ConnStatus] RequestWithBetaProtocol() { <# Minecraft Beta 1.8 to Release 1.3 SLP protocol See https://wiki.vg/Server_List_Ping#Beta_1.8_to_1.3 #> $tcpclient = New-Object System.Net.Sockets.tcpclient $tcpclient.ReceiveTimeout = $this.Timeout * 1000 $tcpclient.SendTimeout = $this.Timeout * 1000 $stopwatch = New-Object System.Diagnostics.Stopwatch $stopwatch.Start(); $result = $tcpclient.BeginConnect($this.Address, $this.Port, $null, $null) $isResponsive = $result.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($this.Timeout)) if (-not $isResponsive) { $this.latency = -1 return [ConnStatus]::Timeout } try { $tcpclient.EndConnect($result) } catch [System.Net.Sockets.SocketException] { return [ConnStatus]::ConnFail } $stopwatch.Stop() if ($this.latency -eq -1) { $this.latency = $stopwatch.ElapsedMilliseconds } $stream = $tcpclient.GetStream() [byte[]] $betaPingPacket = 0xFE try { $stream.Write($betaPingPacket, 0, $betaPingPacket.Length) [byte[]] $responsePacketHeader = $this.NetStreamReadExact( $stream, 3) } catch { return [ConnStatus]::Unknown } if ($responsePacketHeader[0] -ne 0xFF) { return [ConnStatus]::InvalidResponse } if ([System.BitConverter]::IsLittleEndian) { [System.Array]::Reverse($responsePacketHeader); } $payloadLength = [System.BitConverter]::ToUInt16($responsePacketHeader, 0) [byte[]]$payload = $this.NetStreamReadExact($stream, ($payloadLength * 2)) return $this.ParseBetaProtocol($payload) } hidden [ConnStatus] ParseBetaProtocol([byte[]]$rawPayload) { $payloadString = [System.Text.Encoding]::BigEndianUnicode.GetString($rawPayload, 0, $rawPayload.Length) $payloadArray = $payloadString.Split([char]0x00A7) if ($payloadArray.Length -lt 3) { return [ConnStatus]::InvalidResponse } $this.Version = "<= 1.3"; $this.max_players = $payloadArray[$payloadArray.Length - 1]; $this.current_players = $payloadArray[$payloadArray.Length - 2]; $this.motd = $payloadArray[0..($payloadArray.Length - 3)] -join [char]0x00A7 $this.stripped_motd, $this.formatted_motd = $this.generateMotds($this.motd) $this.Slp_Protocol = "Beta"; $this.Online = $true return [ConnStatus]::Success } hidden [byte[]] NetStreamReadExact([System.Net.Sockets.NetworkStream]$stream, [int]$size) { $totalReadBytes = 0 $resultBuffer = New-Object System.Collections.Generic.List[byte] do { $tempBuffer = [System.Byte[]]::CreateInstance([System.Byte], $size - $totalReadBytes) $readBytes = $stream.Read($tempBuffer, 0, $size - $totalReadBytes) if ($readBytes -eq 0) { throw [System.IO.IOException] } $resultBuffer.AddRange($tempBuffer) $totalReadBytes += $readBytes; } while ($totalReadBytes -lt $size) return $resultBuffer.ToArray(); } } } |